/ tests / utils / programmatic-scorer.test.js
programmatic-scorer.test.js
  1  import { describe, it } from 'node:test';
  2  import assert from 'node:assert/strict';
  3  import {
  4    scoreHeadlineQuality,
  5    scoreValueProposition,
  6    scoreUSP,
  7    scoreCTA,
  8    scoreUrgency,
  9    scoreHook,
 10    scoreTrustSignals,
 11    scoreImageryDesign,
 12    scoreOfferClarity,
 13    scoreContext,
 14    detectErrorPage,
 15    detectBusinessDirectory,
 16    classifyIndustry,
 17    extractLocation,
 18    scoreWebsiteProgrammatically,
 19  } from '../../src/utils/programmatic-scorer.js';
 20  
 21  describe('Programmatic Scorer', () => {
 22    describe('scoreHeadlineQuality', () => {
 23      it('returns 0 for missing h1', () => {
 24        const result = scoreHeadlineQuality('<html><body><p>No headline</p></body></html>');
 25        assert.equal(result.score, 0);
 26      });
 27  
 28      it('returns 1 for empty h1', () => {
 29        const result = scoreHeadlineQuality('<html><h1></h1></html>');
 30        assert.equal(result.score, 1);
 31      });
 32  
 33      it('scores higher for benefit language', () => {
 34        const basic = scoreHeadlineQuality('<h1>Plumbing</h1>');
 35        const benefit = scoreHeadlineQuality(
 36          '<h1>Save Money with Professional Plumbing Solutions</h1>'
 37        );
 38        assert.ok(
 39          benefit.score > basic.score,
 40          `benefit ${benefit.score} should be > basic ${basic.score}`
 41        );
 42      });
 43  
 44      it('scores higher for quantified claims', () => {
 45        const noNum = scoreHeadlineQuality('<h1>Best Plumber in Sydney</h1>');
 46        const withNum = scoreHeadlineQuality('<h1>Best Plumber in Sydney - 20 Years Experience</h1>');
 47        assert.ok(withNum.score >= noNum.score);
 48      });
 49    });
 50  
 51    describe('scoreValueProposition', () => {
 52      it('scores higher with quantified claims', () => {
 53        const basic = scoreValueProposition('<p>We do good work</p>');
 54        const quantified = scoreValueProposition(
 55          '<p>Save 30% on your energy bills. Reduce costs by 50% in just 3 months.</p>'
 56        );
 57        assert.ok(quantified.score > basic.score);
 58      });
 59  
 60      it('scores higher with customer-centric language', () => {
 61        const weFocused = scoreValueProposition(
 62          '<p>We are the best. We have experience. Our team delivers.</p>'
 63        );
 64        const youFocused = scoreValueProposition(
 65          '<p>Your business deserves better. You will see results.</p>'
 66        );
 67        assert.ok(youFocused.score >= weFocused.score);
 68      });
 69    });
 70  
 71    describe('scoreUSP', () => {
 72      it('scores higher with differentiation keywords', () => {
 73        const generic = scoreUSP('<p>We provide services</p>');
 74        const unique = scoreUSP(
 75          '<p>The only award-winning plumber in Sydney with 25+ years experience</p>'
 76        );
 77        assert.ok(unique.score > generic.score);
 78      });
 79    });
 80  
 81    describe('scoreCTA', () => {
 82      it('scores base for no CTAs', () => {
 83        const result = scoreCTA('<p>Just text content</p>');
 84        assert.equal(result.score, 2);
 85      });
 86  
 87      it('scores for tel: links', () => {
 88        const result = scoreCTA('<a href="tel:+61412345678">Call Now</a>');
 89        assert.ok(result.score >= 2);
 90      });
 91  
 92      it('scores for forms', () => {
 93        const result = scoreCTA('<form action="/contact"><button>Submit</button></form>');
 94        assert.ok(result.score >= 2);
 95      });
 96  
 97      it('scores for CTA text patterns', () => {
 98        const result = scoreCTA('<a href="#" class="btn">Get a Free Quote</a>');
 99        assert.ok(result.score >= 1);
100      });
101    });
102  
103    describe('scoreUrgency', () => {
104      it('returns base for no urgency', () => {
105        const result = scoreUrgency('<p>Regular content about plumbing services</p>');
106        assert.equal(result.score, 1);
107      });
108  
109      it('scores for time-bound urgency', () => {
110        const result = scoreUrgency('<p>Limited time offer - act now!</p>');
111        assert.ok(result.score >= 4);
112      });
113  
114      it('scores for discounts', () => {
115        const result = scoreUrgency('<p>Save 20% off today</p>');
116        assert.ok(result.score >= 2);
117      });
118    });
119  
120    describe('scoreHook', () => {
121      it('scores higher with video', () => {
122        const noVideo = scoreHook('<p>Just text</p>');
123        const withVideo = scoreHook('<video src="intro.mp4"></video><img src="hero.jpg">');
124        assert.ok(withVideo.score > noVideo.score);
125      });
126  
127      it('scores for images', () => {
128        const result = scoreHook('<img src="hero.jpg" alt="hero"><p>Content here</p>');
129        assert.ok(result.score >= 3);
130      });
131    });
132  
133    describe('scoreTrustSignals', () => {
134      it('returns 0 for no trust signals', () => {
135        const result = scoreTrustSignals('<p>Just basic text about services</p>');
136        assert.equal(result.score, 0);
137      });
138  
139      it('scores for testimonials', () => {
140        const result = scoreTrustSignals('<div class="testimonial">Great service! 5/5 stars</div>');
141        assert.ok(result.score >= 2);
142      });
143  
144      it('scores for certifications', () => {
145        const result = scoreTrustSignals('<p>Licensed and insured since 2005. BBB Accredited.</p>');
146        assert.ok(result.score >= 3);
147      });
148  
149      it('scores for guarantees', () => {
150        const result = scoreTrustSignals('<p>100% satisfaction guarantee</p>');
151        assert.ok(result.score >= 1);
152      });
153    });
154  
155    describe('scoreImageryDesign', () => {
156      it('scores for responsive design', () => {
157        const result = scoreImageryDesign(
158          '<html><head><meta name="viewport" content="width=device-width"></head><body><img src="a.jpg" alt="photo"><img src="b.jpg" alt="photo"></body></html>'
159        );
160        assert.ok(result.score >= 5);
161      });
162    });
163  
164    describe('scoreOfferClarity', () => {
165      it('scores for pricing', () => {
166        const result = scoreOfferClarity('<p>Starting at $99/month. Our services include...</p>');
167        assert.ok(result.score >= 5);
168      });
169    });
170  
171    describe('scoreContext', () => {
172      it('scores higher with keyword match', () => {
173        const noKeyword = scoreContext(
174          '<p>We offer professional services in the local area</p>',
175          null
176        );
177        const withKeyword = scoreContext(
178          '<p>We offer professional plumbing services in the local area of Sydney</p>',
179          'plumber sydney'
180        );
181        assert.ok(withKeyword.score >= noKeyword.score);
182      });
183  
184      it('scores for local indicators', () => {
185        const result = scoreContext('<p>Serving the local community at 123 Main Street</p>', null);
186        assert.ok(result.score >= 5);
187      });
188    });
189  
190    describe('detectErrorPage', () => {
191      it('detects 404 pages', () => {
192        const result = detectErrorPage(
193          '<h1>404 Not Found</h1><p>The page you requested was not found</p>'
194        );
195        assert.equal(result.is_error_page, true);
196      });
197  
198      it('detects parked domains', () => {
199        const result = detectErrorPage('<p>This domain is for sale. Buy this domain now!</p>');
200        assert.equal(result.is_error_page, true);
201      });
202  
203      it('returns false for normal pages', () => {
204        const result = detectErrorPage(
205          '<h1>Welcome to Acme Plumbing</h1><p>Professional plumbing services</p>'
206        );
207        assert.equal(result.is_error_page, false);
208      });
209    });
210  
211    describe('detectBusinessDirectory', () => {
212      it('detects directories', () => {
213        assert.equal(
214          detectBusinessDirectory('<h1>Business Directory</h1><p>Find a business near you</p>'),
215          true
216        );
217      });
218  
219      it('returns false for normal business sites', () => {
220        assert.equal(detectBusinessDirectory('<h1>Acme Plumbing</h1><p>We fix pipes</p>'), false);
221      });
222    });
223  
224    describe('classifyIndustry', () => {
225      it('classifies plumber from keyword', () => {
226        assert.equal(classifyIndustry('', 'plumber sydney'), 'plumber');
227      });
228  
229      it('classifies electrician from keyword', () => {
230        assert.equal(classifyIndustry('', 'electrician melbourne'), 'electrician');
231      });
232  
233      it('classifies from page content when no keyword match', () => {
234        assert.equal(
235          classifyIndustry(
236            '<p>roof repair shingle replacement gutter cleaning roofing services</p>',
237            'services nearby'
238          ),
239          'roofing'
240        );
241      });
242  
243      it('returns general_business for unknown', () => {
244        assert.equal(classifyIndustry('<p>We sell things</p>', 'stuff'), 'general_business');
245      });
246    });
247  
248    describe('extractLocation', () => {
249      it('extracts US city/state', () => {
250        const result = extractLocation('<p>Located in Denver, CO serving the Front Range</p>');
251        assert.equal(result.city, 'Denver');
252        assert.equal(result.state, 'CO');
253      });
254  
255      it('extracts AU city/state', () => {
256        const result = extractLocation('<p>Based in Sydney, NSW</p>');
257        assert.equal(result.city, 'Sydney');
258        assert.equal(result.state, 'NSW');
259      });
260  
261      it('returns null for no location', () => {
262        const result = extractLocation('<p>We provide great services</p>');
263        assert.equal(result.city, null);
264        assert.equal(result.state, null);
265      });
266    });
267  
268    describe('scoreWebsiteProgrammatically', () => {
269      it('returns valid score structure for a real page', () => {
270        const html = `<html><head><meta name="viewport" content="width=device-width"></head>
271          <body>
272            <h1>Best Plumber in Sydney - Fast &amp; Reliable</h1>
273            <p>Save 20% on your first job. Licensed and insured since 2015.</p>
274            <a href="tel:+61412345678" class="btn">Call Now</a>
275            <a href="#" class="btn">Get a Free Quote</a>
276            <img src="hero.jpg" alt="plumber working">
277            <div>Testimonial: Great work! 5/5 - John</div>
278            <form action="/contact"><button>Submit</button></form>
279          </body></html>`;
280  
281        const result = scoreWebsiteProgrammatically(html, 'https://example.com.au', 'plumber sydney');
282  
283        assert.ok(typeof result.conversion_score === 'number');
284        assert.ok(result.conversion_score >= 0 && result.conversion_score <= 100);
285        assert.ok(typeof result.letter_grade === 'string');
286        assert.ok(result.factor_scores !== null);
287        assert.equal(Object.keys(result.factor_scores).length, 10);
288        assert.equal(result.is_error_page, false);
289        assert.equal(result.is_broken_site, false);
290        assert.equal(result.industry_classification, 'plumber');
291        assert.equal(result.country_code, 'AU');
292      });
293  
294      it('handles broken/empty sites', () => {
295        const result = scoreWebsiteProgrammatically('', 'https://example.com', null);
296        assert.equal(result.conversion_score, 0);
297        assert.equal(result.letter_grade, 'F');
298        assert.equal(result.is_broken_site, true);
299      });
300  
301      it('detects error pages', () => {
302        const result = scoreWebsiteProgrammatically(
303          '<h1>404 Not Found</h1><p>Page not found</p>',
304          'https://example.com',
305          null
306        );
307        assert.equal(result.is_error_page, true);
308        assert.equal(result.conversion_score, 0);
309      });
310  
311      it('scores are in expected ranges', () => {
312        const html = `<html><head><title>Acme Services</title></head><body>
313          <h1>Welcome to Our Company</h1>
314          <p>We provide professional services to the community. Our team has been serving local customers for over a decade with quality workmanship and dedication to excellence.</p>
315          <p>Contact us today for a free consultation about your project needs.</p>
316          <a href="/contact">Contact Us</a>
317          <footer>Copyright 2025 Acme Services. All rights reserved.</footer>
318        </body></html>`;
319  
320        const result = scoreWebsiteProgrammatically(html, 'https://example.com', null);
321        // Minimal page should score low
322        assert.ok(
323          result.conversion_score < 70,
324          `Score ${result.conversion_score} should be < 70 for minimal page`
325        );
326        assert.ok(
327          result.conversion_score > 10,
328          `Score ${result.conversion_score} should be > 10 for a page with content`
329        );
330      });
331  
332      it('each factor score is 0-10', () => {
333        const html =
334          '<html><head><title>Test</title></head><body><h1>Hello World</h1><p>Content here with enough text to not be considered a broken site. This is a normal business page with some content about services and offerings for the local community.</p></body></html>';
335        const result = scoreWebsiteProgrammatically(html, 'https://example.com', null);
336  
337        for (const [name, factor] of Object.entries(result.factor_scores)) {
338          assert.ok(factor.score >= 0, `${name} score ${factor.score} should be >= 0`);
339          assert.ok(factor.score <= 10, `${name} score ${factor.score} should be <= 10`);
340          assert.ok(typeof factor.reasoning === 'string', `${name} should have reasoning`);
341          assert.ok(typeof factor.evidence === 'string', `${name} should have evidence`);
342        }
343      });
344    });
345  });