@ucptools/validator 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +60 -0
- package/.vercel/README.txt +11 -0
- package/.vercel/project.json +1 -0
- package/dist/cli/index.d.ts +6 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +279 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/compliance/compliance-generator.d.ts +34 -0
- package/dist/compliance/compliance-generator.d.ts.map +1 -0
- package/dist/compliance/compliance-generator.js +320 -0
- package/dist/compliance/compliance-generator.js.map +1 -0
- package/dist/compliance/index.d.ts +8 -0
- package/dist/compliance/index.d.ts.map +1 -0
- package/dist/compliance/index.js +17 -0
- package/dist/compliance/index.js.map +1 -0
- package/dist/compliance/templates.d.ts +34 -0
- package/dist/compliance/templates.d.ts.map +1 -0
- package/{src/compliance/templates.ts → dist/compliance/templates.js} +117 -155
- package/dist/compliance/templates.js.map +1 -0
- package/dist/compliance/types.d.ts +64 -0
- package/dist/compliance/types.d.ts.map +1 -0
- package/dist/compliance/types.js +64 -0
- package/dist/compliance/types.js.map +1 -0
- package/dist/db/index.d.ts +11 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +63 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/schema.d.ts +444 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +65 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/feed-analyzer/feed-analyzer.d.ts +26 -0
- package/dist/feed-analyzer/feed-analyzer.d.ts.map +1 -0
- package/{src/feed-analyzer/feed-analyzer.ts → dist/feed-analyzer/feed-analyzer.js} +642 -726
- package/dist/feed-analyzer/feed-analyzer.js.map +1 -0
- package/dist/feed-analyzer/index.d.ts +8 -0
- package/dist/feed-analyzer/index.d.ts.map +1 -0
- package/dist/feed-analyzer/index.js +19 -0
- package/dist/feed-analyzer/index.js.map +1 -0
- package/dist/feed-analyzer/types.d.ts +204 -0
- package/dist/feed-analyzer/types.d.ts.map +1 -0
- package/dist/feed-analyzer/types.js +162 -0
- package/dist/feed-analyzer/types.js.map +1 -0
- package/{src/generator/index.ts → dist/generator/index.d.ts} +1 -1
- package/dist/generator/index.d.ts.map +1 -0
- package/dist/generator/index.js +13 -0
- package/dist/generator/index.js.map +1 -0
- package/dist/generator/key-generator.d.ts +24 -0
- package/dist/generator/key-generator.d.ts.map +1 -0
- package/dist/generator/key-generator.js +144 -0
- package/dist/generator/key-generator.js.map +1 -0
- package/dist/generator/profile-builder.d.ts +15 -0
- package/dist/generator/profile-builder.d.ts.map +1 -0
- package/dist/generator/profile-builder.js +338 -0
- package/dist/generator/profile-builder.js.map +1 -0
- package/dist/hosting/artifacts-generator.d.ts +10 -0
- package/dist/hosting/artifacts-generator.d.ts.map +1 -0
- package/{src/hosting/artifacts-generator.ts → dist/hosting/artifacts-generator.js} +191 -241
- package/dist/hosting/artifacts-generator.js.map +1 -0
- package/{src/hosting/index.ts → dist/hosting/index.d.ts} +1 -1
- package/dist/hosting/index.d.ts.map +1 -0
- package/dist/hosting/index.js +10 -0
- package/dist/hosting/index.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +78 -0
- package/dist/index.js.map +1 -0
- package/{src/security/index.ts → dist/security/index.d.ts} +8 -15
- package/dist/security/index.d.ts.map +1 -0
- package/dist/security/index.js +12 -0
- package/dist/security/index.js.map +1 -0
- package/dist/security/security-scanner.d.ts +10 -0
- package/dist/security/security-scanner.d.ts.map +1 -0
- package/dist/security/security-scanner.js +541 -0
- package/dist/security/security-scanner.js.map +1 -0
- package/dist/security/types.d.ts +48 -0
- package/dist/security/types.d.ts.map +1 -0
- package/dist/security/types.js +21 -0
- package/dist/security/types.js.map +1 -0
- package/dist/services/directory.d.ts +104 -0
- package/dist/services/directory.d.ts.map +1 -0
- package/dist/services/directory.js +333 -0
- package/dist/services/directory.js.map +1 -0
- package/dist/simulator/agent-simulator.d.ts +69 -0
- package/dist/simulator/agent-simulator.d.ts.map +1 -0
- package/{src/simulator/agent-simulator.ts → dist/simulator/agent-simulator.js} +650 -941
- package/dist/simulator/agent-simulator.js.map +1 -0
- package/{src/simulator/index.ts → dist/simulator/index.d.ts} +7 -7
- package/dist/simulator/index.d.ts.map +1 -0
- package/dist/simulator/index.js +23 -0
- package/dist/simulator/index.js.map +1 -0
- package/{src/simulator/types.ts → dist/simulator/types.d.ts} +145 -170
- package/dist/simulator/types.d.ts.map +1 -0
- package/dist/simulator/types.js +18 -0
- package/dist/simulator/types.js.map +1 -0
- package/dist/types/generator.d.ts +106 -0
- package/dist/types/generator.d.ts.map +1 -0
- package/dist/types/generator.js +6 -0
- package/dist/types/generator.js.map +1 -0
- package/{src/types/index.ts → dist/types/index.d.ts} +1 -1
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +23 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/ucp-profile.d.ts +103 -0
- package/dist/types/ucp-profile.d.ts.map +1 -0
- package/dist/types/ucp-profile.js +45 -0
- package/dist/types/ucp-profile.js.map +1 -0
- package/dist/types/validation.d.ts +68 -0
- package/dist/types/validation.d.ts.map +1 -0
- package/dist/types/validation.js +32 -0
- package/dist/types/validation.js.map +1 -0
- package/dist/validator/index.d.ts +26 -0
- package/dist/validator/index.d.ts.map +1 -0
- package/dist/validator/index.js +161 -0
- package/dist/validator/index.js.map +1 -0
- package/dist/validator/network-validator.d.ts +28 -0
- package/dist/validator/network-validator.d.ts.map +1 -0
- package/dist/validator/network-validator.js +319 -0
- package/dist/validator/network-validator.js.map +1 -0
- package/dist/validator/rules-validator.d.ts +11 -0
- package/dist/validator/rules-validator.d.ts.map +1 -0
- package/dist/validator/rules-validator.js +257 -0
- package/dist/validator/rules-validator.js.map +1 -0
- package/dist/validator/sdk-validator.d.ts +58 -0
- package/dist/validator/sdk-validator.d.ts.map +1 -0
- package/{src/validator/sdk-validator.ts → dist/validator/sdk-validator.js} +273 -330
- package/dist/validator/sdk-validator.js.map +1 -0
- package/dist/validator/structural-validator.d.ts +11 -0
- package/dist/validator/structural-validator.d.ts.map +1 -0
- package/dist/validator/structural-validator.js +415 -0
- package/dist/validator/structural-validator.js.map +1 -0
- package/package.json +1 -1
- package/publish-output.txt +0 -0
- package/CLAUDE.md +0 -109
- package/api/analyze-feed.js +0 -140
- package/api/badge.js +0 -185
- package/api/benchmark.js +0 -177
- package/api/directory-stats.ts +0 -29
- package/api/directory.ts +0 -73
- package/api/generate-compliance.js +0 -143
- package/api/generate-schema.js +0 -457
- package/api/generate.js +0 -132
- package/api/security-scan.js +0 -133
- package/api/simulate.js +0 -187
- package/api/tsconfig.json +0 -10
- package/api/validate.js +0 -1351
- package/apify-actor/.actor/actor.json +0 -68
- package/apify-actor/.actor/input_schema.json +0 -32
- package/apify-actor/APIFY-STORE-LISTING.md +0 -412
- package/apify-actor/Dockerfile +0 -8
- package/apify-actor/README.md +0 -166
- package/apify-actor/main.ts +0 -111
- package/apify-actor/package.json +0 -17
- package/apify-actor/src/main.js +0 -199
- package/docs/BRAND-IDENTITY.md +0 -238
- package/docs/BRAND-STYLE-GUIDE.md +0 -356
- package/drizzle/0000_black_king_cobra.sql +0 -39
- package/drizzle/meta/0000_snapshot.json +0 -309
- package/drizzle/meta/_journal.json +0 -13
- package/drizzle.config.ts +0 -10
- package/public/.well-known/ucp +0 -25
- package/public/android-chrome-192x192.png +0 -0
- package/public/android-chrome-512x512.png +0 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/brand.css +0 -321
- package/public/directory.html +0 -701
- package/public/favicon-16x16.png +0 -0
- package/public/favicon-32x32.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/guides/bigcommerce.html +0 -743
- package/public/guides/fastucp.html +0 -838
- package/public/guides/magento.html +0 -779
- package/public/guides/shopify.html +0 -726
- package/public/guides/squarespace.html +0 -749
- package/public/guides/wix.html +0 -747
- package/public/guides/woocommerce.html +0 -733
- package/public/index.html +0 -3835
- package/public/learn.html +0 -396
- package/public/logo.jpeg +0 -0
- package/public/og-image-icon.png +0 -0
- package/public/og-image.png +0 -0
- package/public/robots.txt +0 -6
- package/public/site.webmanifest +0 -31
- package/public/sitemap.xml +0 -69
- package/public/social/linkedin-banner-1128x191.png +0 -0
- package/public/social/temp.PNG +0 -0
- package/public/social/x-header-1500x500.png +0 -0
- package/public/verify.html +0 -410
- package/scripts/generate-favicons.js +0 -44
- package/scripts/generate-ico.js +0 -23
- package/scripts/generate-og-image.js +0 -45
- package/scripts/reset-db.ts +0 -77
- package/scripts/seed-db.ts +0 -71
- package/scripts/setup-benchmark-db.js +0 -70
- package/src/api/server.ts +0 -266
- package/src/cli/index.ts +0 -302
- package/src/compliance/compliance-generator.ts +0 -452
- package/src/compliance/index.ts +0 -28
- package/src/compliance/types.ts +0 -170
- package/src/db/index.ts +0 -28
- package/src/db/schema.ts +0 -84
- package/src/feed-analyzer/index.ts +0 -34
- package/src/feed-analyzer/types.ts +0 -354
- package/src/generator/key-generator.ts +0 -124
- package/src/generator/profile-builder.ts +0 -402
- package/src/index.ts +0 -105
- package/src/security/security-scanner.ts +0 -604
- package/src/security/types.ts +0 -55
- package/src/services/directory.ts +0 -434
- package/src/types/generator.ts +0 -140
- package/src/types/ucp-profile.ts +0 -140
- package/src/types/validation.ts +0 -89
- package/src/validator/index.ts +0 -194
- package/src/validator/network-validator.ts +0 -417
- package/src/validator/rules-validator.ts +0 -297
- package/src/validator/structural-validator.ts +0 -476
- package/tests/fixtures/non-compliant-profile.json +0 -25
- package/tests/fixtures/official-sample-profile.json +0 -75
- package/tests/integration/benchmark.test.ts +0 -207
- package/tests/integration/database.test.ts +0 -163
- package/tests/integration/directory-api.test.ts +0 -268
- package/tests/integration/simulate-api.test.ts +0 -230
- package/tests/integration/validate-api.test.ts +0 -269
- package/tests/setup.ts +0 -15
- package/tests/unit/agent-simulator.test.ts +0 -575
- package/tests/unit/compliance-generator.test.ts +0 -374
- package/tests/unit/directory-service.test.ts +0 -272
- package/tests/unit/feed-analyzer.test.ts +0 -517
- package/tests/unit/lint-suggestions.test.ts +0 -423
- package/tests/unit/official-samples.test.ts +0 -211
- package/tests/unit/pdf-report.test.ts +0 -390
- package/tests/unit/sdk-validator.test.ts +0 -531
- package/tests/unit/security-scanner.test.ts +0 -410
- package/tests/unit/validation.test.ts +0 -390
- package/vercel.json +0 -34
- package/vitest.config.ts +0 -22
|
@@ -1,517 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for Product Feed Quality Analyzer
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { describe, it, expect } from 'vitest';
|
|
6
|
-
import {
|
|
7
|
-
validateGtin,
|
|
8
|
-
extractProductsFromHtml,
|
|
9
|
-
analyzeProduct,
|
|
10
|
-
analyzeProductFeed,
|
|
11
|
-
analyzeProductFeedFromHtml,
|
|
12
|
-
QUALITY_CHECKS,
|
|
13
|
-
VALID_AVAILABILITY_VALUES,
|
|
14
|
-
CATEGORY_WEIGHTS,
|
|
15
|
-
GRADE_THRESHOLDS,
|
|
16
|
-
} from '../../src/feed-analyzer/index.js';
|
|
17
|
-
import type { ProductData } from '../../src/feed-analyzer/index.js';
|
|
18
|
-
|
|
19
|
-
describe('Feed Analyzer', () => {
|
|
20
|
-
describe('validateGtin', () => {
|
|
21
|
-
it('should validate a correct GTIN-13 (EAN)', () => {
|
|
22
|
-
const result = validateGtin('4006381333931');
|
|
23
|
-
expect(result.isValid).toBe(true);
|
|
24
|
-
expect(result.type).toBe('EAN');
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('should validate a correct UPC-12', () => {
|
|
28
|
-
const result = validateGtin('012345678905');
|
|
29
|
-
expect(result.isValid).toBe(true);
|
|
30
|
-
expect(result.type).toBe('UPC');
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('should validate a correct GTIN-8', () => {
|
|
34
|
-
const result = validateGtin('96385074');
|
|
35
|
-
expect(result.isValid).toBe(true);
|
|
36
|
-
expect(result.type).toBe('GTIN-8');
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('should validate a correct GTIN-14', () => {
|
|
40
|
-
const result = validateGtin('00012345678905');
|
|
41
|
-
expect(result.isValid).toBe(true);
|
|
42
|
-
expect(result.type).toBe('GTIN-14');
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('should reject invalid GTIN with wrong check digit', () => {
|
|
46
|
-
const result = validateGtin('4006381333932'); // Wrong check digit
|
|
47
|
-
expect(result.isValid).toBe(false);
|
|
48
|
-
expect(result.error).toContain('check digit');
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('should reject GTIN with invalid length', () => {
|
|
52
|
-
const result = validateGtin('12345');
|
|
53
|
-
expect(result.isValid).toBe(false);
|
|
54
|
-
expect(result.error).toContain('Invalid GTIN length');
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('should reject GTIN with non-numeric characters', () => {
|
|
58
|
-
const result = validateGtin('400638133393A');
|
|
59
|
-
expect(result.isValid).toBe(false);
|
|
60
|
-
expect(result.error).toContain('digits');
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('should reject empty GTIN', () => {
|
|
64
|
-
const result = validateGtin('');
|
|
65
|
-
expect(result.isValid).toBe(false);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('should handle GTIN with spaces', () => {
|
|
69
|
-
const result = validateGtin('4006 3813 3393 1');
|
|
70
|
-
expect(result.isValid).toBe(true);
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
describe('extractProductsFromHtml', () => {
|
|
75
|
-
it('should extract products from JSON-LD script', () => {
|
|
76
|
-
const html = `
|
|
77
|
-
<html>
|
|
78
|
-
<head>
|
|
79
|
-
<script type="application/ld+json">
|
|
80
|
-
{
|
|
81
|
-
"@type": "Product",
|
|
82
|
-
"name": "Test Product",
|
|
83
|
-
"sku": "TEST-001"
|
|
84
|
-
}
|
|
85
|
-
</script>
|
|
86
|
-
</head>
|
|
87
|
-
</html>
|
|
88
|
-
`;
|
|
89
|
-
const products = extractProductsFromHtml(html);
|
|
90
|
-
expect(products).toHaveLength(1);
|
|
91
|
-
expect(products[0].name).toBe('Test Product');
|
|
92
|
-
expect(products[0].sku).toBe('TEST-001');
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('should extract products from @graph structure', () => {
|
|
96
|
-
const html = `
|
|
97
|
-
<script type="application/ld+json">
|
|
98
|
-
{
|
|
99
|
-
"@context": "https://schema.org",
|
|
100
|
-
"@graph": [
|
|
101
|
-
{"@type": "WebSite", "name": "Store"},
|
|
102
|
-
{"@type": "Product", "name": "Product 1"},
|
|
103
|
-
{"@type": "Product", "name": "Product 2"}
|
|
104
|
-
]
|
|
105
|
-
}
|
|
106
|
-
</script>
|
|
107
|
-
`;
|
|
108
|
-
const products = extractProductsFromHtml(html);
|
|
109
|
-
expect(products).toHaveLength(2);
|
|
110
|
-
expect(products[0].name).toBe('Product 1');
|
|
111
|
-
expect(products[1].name).toBe('Product 2');
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('should extract products from ItemList', () => {
|
|
115
|
-
const html = `
|
|
116
|
-
<script type="application/ld+json">
|
|
117
|
-
{
|
|
118
|
-
"@type": "ItemList",
|
|
119
|
-
"itemListElement": [
|
|
120
|
-
{"@type": "ListItem", "item": {"@type": "Product", "name": "Item 1"}},
|
|
121
|
-
{"@type": "ListItem", "item": {"@type": "Product", "name": "Item 2"}}
|
|
122
|
-
]
|
|
123
|
-
}
|
|
124
|
-
</script>
|
|
125
|
-
`;
|
|
126
|
-
const products = extractProductsFromHtml(html);
|
|
127
|
-
expect(products).toHaveLength(2);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it('should handle multiple JSON-LD scripts', () => {
|
|
131
|
-
const html = `
|
|
132
|
-
<script type="application/ld+json">{"@type": "Product", "name": "P1"}</script>
|
|
133
|
-
<script type="application/ld+json">{"@type": "Product", "name": "P2"}</script>
|
|
134
|
-
`;
|
|
135
|
-
const products = extractProductsFromHtml(html);
|
|
136
|
-
expect(products).toHaveLength(2);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('should skip invalid JSON', () => {
|
|
140
|
-
const html = `
|
|
141
|
-
<script type="application/ld+json">{invalid json}</script>
|
|
142
|
-
<script type="application/ld+json">{"@type": "Product", "name": "Valid"}</script>
|
|
143
|
-
`;
|
|
144
|
-
const products = extractProductsFromHtml(html);
|
|
145
|
-
expect(products).toHaveLength(1);
|
|
146
|
-
expect(products[0].name).toBe('Valid');
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it('should return empty array when no products found', () => {
|
|
150
|
-
const html = `<html><body>No products here</body></html>`;
|
|
151
|
-
const products = extractProductsFromHtml(html);
|
|
152
|
-
expect(products).toHaveLength(0);
|
|
153
|
-
});
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
describe('analyzeProduct', () => {
|
|
157
|
-
const completeProduct: ProductData = {
|
|
158
|
-
name: 'Complete Product',
|
|
159
|
-
description: 'This is a complete product description with enough characters to pass the minimum length requirement for good SEO.',
|
|
160
|
-
sku: 'SKU-001',
|
|
161
|
-
gtin: '4006381333931',
|
|
162
|
-
brand: { name: 'Test Brand' },
|
|
163
|
-
image: ['https://example.com/img1.jpg', 'https://example.com/img2.jpg'],
|
|
164
|
-
category: 'Electronics',
|
|
165
|
-
offers: {
|
|
166
|
-
'@type': 'Offer',
|
|
167
|
-
price: 99.99,
|
|
168
|
-
priceCurrency: 'USD',
|
|
169
|
-
availability: 'https://schema.org/InStock',
|
|
170
|
-
},
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
it('should analyze a complete product with high score', () => {
|
|
174
|
-
const result = analyzeProduct(completeProduct);
|
|
175
|
-
expect(result.score).toBeGreaterThanOrEqual(90);
|
|
176
|
-
expect(result.issues).toHaveLength(0);
|
|
177
|
-
expect(result.attributes.hasName).toBe(true);
|
|
178
|
-
expect(result.attributes.hasDescription).toBe(true);
|
|
179
|
-
expect(result.attributes.hasSku).toBe(true);
|
|
180
|
-
expect(result.attributes.hasGtin).toBe(true);
|
|
181
|
-
expect(result.attributes.hasBrand).toBe(true);
|
|
182
|
-
expect(result.attributes.hasImage).toBe(true);
|
|
183
|
-
expect(result.attributes.hasPrice).toBe(true);
|
|
184
|
-
expect(result.attributes.hasAvailability).toBe(true);
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
it('should flag missing name as critical', () => {
|
|
188
|
-
const product: ProductData = { ...completeProduct, name: undefined };
|
|
189
|
-
const result = analyzeProduct(product);
|
|
190
|
-
const nameIssue = result.issues.find(i => i.id === 'missing-name');
|
|
191
|
-
expect(nameIssue).toBeDefined();
|
|
192
|
-
expect(nameIssue?.severity).toBe('critical');
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
it('should flag missing description as warning', () => {
|
|
196
|
-
const product: ProductData = { ...completeProduct, description: undefined };
|
|
197
|
-
const result = analyzeProduct(product);
|
|
198
|
-
const descIssue = result.issues.find(i => i.id === 'missing-description');
|
|
199
|
-
expect(descIssue).toBeDefined();
|
|
200
|
-
expect(descIssue?.severity).toBe('warning');
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
it('should flag short description', () => {
|
|
204
|
-
const product: ProductData = { ...completeProduct, description: 'Short desc' };
|
|
205
|
-
const result = analyzeProduct(product);
|
|
206
|
-
const shortIssue = result.issues.find(i => i.id === 'short-description');
|
|
207
|
-
expect(shortIssue).toBeDefined();
|
|
208
|
-
expect(shortIssue?.severity).toBe('info');
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
it('should flag missing price as critical', () => {
|
|
212
|
-
const product: ProductData = { ...completeProduct, offers: undefined };
|
|
213
|
-
const result = analyzeProduct(product);
|
|
214
|
-
const priceIssue = result.issues.find(i => i.id === 'missing-price');
|
|
215
|
-
expect(priceIssue).toBeDefined();
|
|
216
|
-
expect(priceIssue?.severity).toBe('critical');
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
it('should flag missing image as critical', () => {
|
|
220
|
-
const product: ProductData = { ...completeProduct, image: undefined };
|
|
221
|
-
const result = analyzeProduct(product);
|
|
222
|
-
const imageIssue = result.issues.find(i => i.id === 'missing-image');
|
|
223
|
-
expect(imageIssue).toBeDefined();
|
|
224
|
-
expect(imageIssue?.severity).toBe('critical');
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
it('should flag single image as info', () => {
|
|
228
|
-
const product: ProductData = { ...completeProduct, image: 'single.jpg' };
|
|
229
|
-
const result = analyzeProduct(product);
|
|
230
|
-
const singleImageIssue = result.issues.find(i => i.id === 'single-image');
|
|
231
|
-
expect(singleImageIssue).toBeDefined();
|
|
232
|
-
expect(singleImageIssue?.severity).toBe('info');
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
it('should flag invalid GTIN', () => {
|
|
236
|
-
const product: ProductData = { ...completeProduct, gtin: '1234567890' };
|
|
237
|
-
const result = analyzeProduct(product);
|
|
238
|
-
const gtinIssue = result.issues.find(i => i.id === 'invalid-gtin');
|
|
239
|
-
expect(gtinIssue).toBeDefined();
|
|
240
|
-
expect(gtinIssue?.severity).toBe('critical');
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
it('should flag missing availability', () => {
|
|
244
|
-
const product: ProductData = {
|
|
245
|
-
...completeProduct,
|
|
246
|
-
offers: { price: 99.99, priceCurrency: 'USD' },
|
|
247
|
-
};
|
|
248
|
-
const result = analyzeProduct(product);
|
|
249
|
-
const availIssue = result.issues.find(i => i.id === 'missing-availability');
|
|
250
|
-
expect(availIssue).toBeDefined();
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
it('should flag invalid availability value', () => {
|
|
254
|
-
const product: ProductData = {
|
|
255
|
-
...completeProduct,
|
|
256
|
-
offers: {
|
|
257
|
-
price: 99.99,
|
|
258
|
-
priceCurrency: 'USD',
|
|
259
|
-
availability: 'available',
|
|
260
|
-
},
|
|
261
|
-
};
|
|
262
|
-
const result = analyzeProduct(product);
|
|
263
|
-
const availIssue = result.issues.find(i => i.id === 'invalid-availability');
|
|
264
|
-
expect(availIssue).toBeDefined();
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
it('should handle string brand', () => {
|
|
268
|
-
const product: ProductData = { ...completeProduct, brand: 'String Brand' };
|
|
269
|
-
const result = analyzeProduct(product);
|
|
270
|
-
expect(result.attributes.hasBrand).toBe(true);
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
it('should handle price as string', () => {
|
|
274
|
-
const product: ProductData = {
|
|
275
|
-
...completeProduct,
|
|
276
|
-
offers: { price: '99.99', priceCurrency: 'USD', availability: 'InStock' },
|
|
277
|
-
};
|
|
278
|
-
const result = analyzeProduct(product);
|
|
279
|
-
expect(result.attributes.hasPrice).toBe(true);
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
it('should flag invalid price format', () => {
|
|
283
|
-
const product: ProductData = {
|
|
284
|
-
...completeProduct,
|
|
285
|
-
offers: { price: 'free', priceCurrency: 'USD' },
|
|
286
|
-
};
|
|
287
|
-
const result = analyzeProduct(product);
|
|
288
|
-
const priceIssue = result.issues.find(i => i.id === 'invalid-price');
|
|
289
|
-
expect(priceIssue).toBeDefined();
|
|
290
|
-
});
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
describe('analyzeProductFeed', () => {
|
|
294
|
-
const products: ProductData[] = [
|
|
295
|
-
{
|
|
296
|
-
name: 'Product 1',
|
|
297
|
-
description: 'A great product with a detailed description that provides all necessary information.',
|
|
298
|
-
sku: 'P1',
|
|
299
|
-
gtin: '4006381333931',
|
|
300
|
-
brand: 'Brand A',
|
|
301
|
-
image: ['img1.jpg', 'img2.jpg'],
|
|
302
|
-
offers: { price: 50, priceCurrency: 'USD', availability: 'InStock' },
|
|
303
|
-
category: 'Category A',
|
|
304
|
-
},
|
|
305
|
-
{
|
|
306
|
-
name: 'Product 2',
|
|
307
|
-
description: 'Another product',
|
|
308
|
-
sku: 'P2',
|
|
309
|
-
image: 'img.jpg',
|
|
310
|
-
offers: { price: 30, priceCurrency: 'USD' },
|
|
311
|
-
},
|
|
312
|
-
{
|
|
313
|
-
name: 'Product 3',
|
|
314
|
-
offers: { price: 20 },
|
|
315
|
-
},
|
|
316
|
-
];
|
|
317
|
-
|
|
318
|
-
it('should analyze multiple products', () => {
|
|
319
|
-
const result = analyzeProductFeed(products, 'https://example.com');
|
|
320
|
-
expect(result.productsFound).toBe(3);
|
|
321
|
-
expect(result.productsAnalyzed).toBe(3);
|
|
322
|
-
expect(result.products).toHaveLength(3);
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
it('should calculate overall score', () => {
|
|
326
|
-
const result = analyzeProductFeed(products, 'https://example.com');
|
|
327
|
-
expect(result.overallScore).toBeGreaterThan(0);
|
|
328
|
-
expect(result.overallScore).toBeLessThanOrEqual(100);
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
it('should calculate agent visibility score', () => {
|
|
332
|
-
const result = analyzeProductFeed(products, 'https://example.com');
|
|
333
|
-
expect(result.agentVisibilityScore).toBeGreaterThan(0);
|
|
334
|
-
expect(result.agentVisibilityScore).toBeLessThanOrEqual(100);
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
it('should assign appropriate grade', () => {
|
|
338
|
-
const result = analyzeProductFeed(products, 'https://example.com');
|
|
339
|
-
expect(['A', 'B', 'C', 'D', 'F']).toContain(result.grade);
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
it('should calculate category scores', () => {
|
|
343
|
-
const result = analyzeProductFeed(products, 'https://example.com');
|
|
344
|
-
expect(result.categoryScores).toBeDefined();
|
|
345
|
-
expect(result.categoryScores.completeness).toBeGreaterThanOrEqual(0);
|
|
346
|
-
expect(result.categoryScores.pricing).toBeGreaterThanOrEqual(0);
|
|
347
|
-
expect(result.categoryScores.images).toBeGreaterThanOrEqual(0);
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
it('should aggregate issues across products', () => {
|
|
351
|
-
const result = analyzeProductFeed(products, 'https://example.com');
|
|
352
|
-
expect(result.issues.length).toBeGreaterThan(0);
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
it('should identify top issues', () => {
|
|
356
|
-
const result = analyzeProductFeed(products, 'https://example.com');
|
|
357
|
-
expect(result.topIssues).toBeDefined();
|
|
358
|
-
// Top issues should be critical or warning severity
|
|
359
|
-
result.topIssues.forEach(issue => {
|
|
360
|
-
expect(['critical', 'warning']).toContain(issue.severity);
|
|
361
|
-
});
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
it('should generate recommendations', () => {
|
|
365
|
-
const result = analyzeProductFeed(products, 'https://example.com');
|
|
366
|
-
expect(result.recommendations.length).toBeGreaterThan(0);
|
|
367
|
-
// Recommendations should have required fields
|
|
368
|
-
result.recommendations.forEach(rec => {
|
|
369
|
-
expect(rec.priority).toBeDefined();
|
|
370
|
-
expect(rec.title).toBeDefined();
|
|
371
|
-
expect(rec.description).toBeDefined();
|
|
372
|
-
expect(rec.affectedCount).toBeGreaterThan(0);
|
|
373
|
-
});
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
it('should calculate summary statistics', () => {
|
|
377
|
-
const result = analyzeProductFeed(products, 'https://example.com');
|
|
378
|
-
expect(result.summary.totalProducts).toBe(3);
|
|
379
|
-
expect(result.summary.withName).toBe(3);
|
|
380
|
-
expect(result.summary.withPrice).toBe(3);
|
|
381
|
-
expect(result.summary.withImages).toBe(2); // Product 3 has no image
|
|
382
|
-
expect(result.summary.withGtin).toBe(1);
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
it('should respect maxProducts limit', () => {
|
|
386
|
-
const result = analyzeProductFeed(products, 'https://example.com', 2);
|
|
387
|
-
expect(result.productsFound).toBe(3);
|
|
388
|
-
expect(result.productsAnalyzed).toBe(2);
|
|
389
|
-
expect(result.products).toHaveLength(2);
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
it('should exclude product details when requested', () => {
|
|
393
|
-
const result = analyzeProductFeed(products, 'https://example.com', 50, false);
|
|
394
|
-
expect(result.products).toHaveLength(0);
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
it('should handle empty product array', () => {
|
|
398
|
-
const result = analyzeProductFeed([], 'https://example.com');
|
|
399
|
-
expect(result.productsFound).toBe(0);
|
|
400
|
-
expect(result.productsAnalyzed).toBe(0);
|
|
401
|
-
expect(result.overallScore).toBe(0);
|
|
402
|
-
expect(result.grade).toBe('F');
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
it('should include analyzed URL', () => {
|
|
406
|
-
const result = analyzeProductFeed(products, 'https://example.com/products');
|
|
407
|
-
expect(result.url).toBe('https://example.com/products');
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
it('should include timestamp', () => {
|
|
411
|
-
const result = analyzeProductFeed(products, 'https://example.com');
|
|
412
|
-
expect(result.analyzedAt).toBeDefined();
|
|
413
|
-
expect(new Date(result.analyzedAt).getTime()).not.toBeNaN();
|
|
414
|
-
});
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
describe('analyzeProductFeedFromHtml', () => {
|
|
418
|
-
it('should analyze products extracted from HTML', () => {
|
|
419
|
-
const html = `
|
|
420
|
-
<script type="application/ld+json">
|
|
421
|
-
{"@type": "Product", "name": "HTML Product", "offers": {"price": 10}}
|
|
422
|
-
</script>
|
|
423
|
-
`;
|
|
424
|
-
const result = analyzeProductFeedFromHtml(html, 'https://example.com');
|
|
425
|
-
expect(result.productsFound).toBe(1);
|
|
426
|
-
expect(result.products[0].name).toBe('HTML Product');
|
|
427
|
-
});
|
|
428
|
-
|
|
429
|
-
it('should pass options through', () => {
|
|
430
|
-
const html = `
|
|
431
|
-
<script type="application/ld+json">{"@type": "Product", "name": "P1"}</script>
|
|
432
|
-
<script type="application/ld+json">{"@type": "Product", "name": "P2"}</script>
|
|
433
|
-
<script type="application/ld+json">{"@type": "Product", "name": "P3"}</script>
|
|
434
|
-
`;
|
|
435
|
-
const result = analyzeProductFeedFromHtml(html, 'https://example.com', {
|
|
436
|
-
maxProducts: 2,
|
|
437
|
-
includeProductDetails: false,
|
|
438
|
-
});
|
|
439
|
-
expect(result.productsAnalyzed).toBe(2);
|
|
440
|
-
expect(result.products).toHaveLength(0);
|
|
441
|
-
});
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
describe('Constants', () => {
|
|
445
|
-
it('should have quality check definitions', () => {
|
|
446
|
-
expect(QUALITY_CHECKS['missing-name']).toBeDefined();
|
|
447
|
-
expect(QUALITY_CHECKS['missing-price']).toBeDefined();
|
|
448
|
-
expect(QUALITY_CHECKS['missing-image']).toBeDefined();
|
|
449
|
-
expect(QUALITY_CHECKS['invalid-gtin']).toBeDefined();
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
it('should have valid availability values', () => {
|
|
453
|
-
expect(VALID_AVAILABILITY_VALUES).toContain('InStock');
|
|
454
|
-
expect(VALID_AVAILABILITY_VALUES).toContain('https://schema.org/InStock');
|
|
455
|
-
expect(VALID_AVAILABILITY_VALUES).toContain('OutOfStock');
|
|
456
|
-
});
|
|
457
|
-
|
|
458
|
-
it('should have category weights summing to 100', () => {
|
|
459
|
-
const totalWeight = Object.values(CATEGORY_WEIGHTS).reduce((a, b) => a + b, 0);
|
|
460
|
-
expect(totalWeight).toBe(100);
|
|
461
|
-
});
|
|
462
|
-
|
|
463
|
-
it('should have grade thresholds in descending order', () => {
|
|
464
|
-
expect(GRADE_THRESHOLDS.A).toBeGreaterThan(GRADE_THRESHOLDS.B);
|
|
465
|
-
expect(GRADE_THRESHOLDS.B).toBeGreaterThan(GRADE_THRESHOLDS.C);
|
|
466
|
-
expect(GRADE_THRESHOLDS.C).toBeGreaterThan(GRADE_THRESHOLDS.D);
|
|
467
|
-
expect(GRADE_THRESHOLDS.D).toBeGreaterThan(GRADE_THRESHOLDS.F);
|
|
468
|
-
});
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
describe('Edge cases', () => {
|
|
472
|
-
it('should handle product with array of offers', () => {
|
|
473
|
-
const product: ProductData = {
|
|
474
|
-
name: 'Multi-offer Product',
|
|
475
|
-
offers: [
|
|
476
|
-
{ price: 100, priceCurrency: 'USD' },
|
|
477
|
-
{ price: 90, priceCurrency: 'EUR' },
|
|
478
|
-
],
|
|
479
|
-
};
|
|
480
|
-
const result = analyzeProduct(product);
|
|
481
|
-
expect(result.attributes.hasPrice).toBe(true);
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
it('should handle product with zero price', () => {
|
|
485
|
-
const product: ProductData = {
|
|
486
|
-
name: 'Free Product',
|
|
487
|
-
offers: { price: 0, priceCurrency: 'USD' },
|
|
488
|
-
};
|
|
489
|
-
const result = analyzeProduct(product);
|
|
490
|
-
expect(result.attributes.hasPrice).toBe(true);
|
|
491
|
-
const priceIssue = result.issues.find(i => i.id === 'missing-price');
|
|
492
|
-
expect(priceIssue).toBeUndefined();
|
|
493
|
-
});
|
|
494
|
-
|
|
495
|
-
it('should handle product with image object array', () => {
|
|
496
|
-
const product: ProductData = {
|
|
497
|
-
name: 'Product with Image Objects',
|
|
498
|
-
offers: { price: 10 },
|
|
499
|
-
image: [{ url: 'img1.jpg' }, { url: 'img2.jpg' }],
|
|
500
|
-
};
|
|
501
|
-
const result = analyzeProduct(product);
|
|
502
|
-
expect(result.attributes.hasImage).toBe(true);
|
|
503
|
-
expect(result.attributes.imageCount).toBe(2);
|
|
504
|
-
});
|
|
505
|
-
|
|
506
|
-
it('should count description length correctly', () => {
|
|
507
|
-
const description = 'A'.repeat(100);
|
|
508
|
-
const product: ProductData = {
|
|
509
|
-
name: 'Product',
|
|
510
|
-
description,
|
|
511
|
-
offers: { price: 10 },
|
|
512
|
-
};
|
|
513
|
-
const result = analyzeProduct(product);
|
|
514
|
-
expect(result.attributes.descriptionLength).toBe(100);
|
|
515
|
-
});
|
|
516
|
-
});
|
|
517
|
-
});
|