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 & 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 });