keyword-validator-supplement.test.js
1 /** 2 * Keyword Validator Supplement Tests 3 * 4 * Covers lines not hit by the existing keyword-validator.test.js: 5 * - analyzeSearchVolumes with empty CSV (lines 651-657) 6 * - generateSearchVolumeCSV: file not found error (line 491) 7 * - getLanguageName function 8 * - getLanguageCode with explicit language override 9 */ 10 import { test, describe } from 'node:test'; 11 import assert from 'node:assert/strict'; 12 import fs from 'fs'; 13 import { 14 getLocationCode, 15 getLanguageCode, 16 getLanguageName, 17 filterKeywordsByVolume, 18 analyzeSearchVolumes, 19 generateSearchVolumeCSV, 20 } from '../../src/utils/keyword-validator.js'; 21 22 describe('getLocationCode', () => { 23 test('returns correct code for all major countries', () => { 24 assert.equal(getLocationCode('US'), 2840); 25 assert.equal(getLocationCode('AU'), 2036); 26 assert.equal(getLocationCode('UK'), 2826); 27 assert.equal(getLocationCode('DE'), 2276); 28 assert.equal(getLocationCode('FR'), 2250); 29 assert.equal(getLocationCode('JP'), 2392); 30 assert.equal(getLocationCode('CA'), 2124); 31 assert.equal(getLocationCode('NZ'), 2554); 32 }); 33 34 test('is case-insensitive', () => { 35 assert.equal(getLocationCode('au'), 2036); 36 assert.equal(getLocationCode('us'), 2840); 37 }); 38 39 test('defaults to Australia for unknown country', () => { 40 assert.equal(getLocationCode('XX'), 2036); 41 assert.equal(getLocationCode('INVALID'), 2036); 42 }); 43 }); 44 45 describe('getLanguageCode', () => { 46 test('returns correct language for countries', () => { 47 assert.equal(getLanguageCode('AU'), 'en'); 48 assert.equal(getLanguageCode('DE'), 'de'); 49 assert.equal(getLanguageCode('FR'), 'fr'); 50 assert.equal(getLanguageCode('JP'), 'ja'); 51 assert.equal(getLanguageCode('CN'), 'zh-CN'); 52 assert.equal(getLanguageCode('KR'), 'ko'); 53 }); 54 55 test('respects explicit language override', () => { 56 assert.equal(getLanguageCode('AU', 'fr'), 'fr'); 57 assert.equal(getLanguageCode('US', 'es'), 'es'); 58 }); 59 60 test('defaults to English for unknown country', () => { 61 assert.equal(getLanguageCode('XX'), 'en'); 62 }); 63 64 test('is case-insensitive', () => { 65 assert.equal(getLanguageCode('au'), 'en'); 66 }); 67 }); 68 69 describe('getLanguageName', () => { 70 test('returns language name for known countries', () => { 71 assert.equal(getLanguageName('AU'), 'English'); 72 assert.equal(getLanguageName('DE'), 'German'); 73 assert.equal(getLanguageName('FR'), 'French'); 74 assert.equal(getLanguageName('JP'), 'Japanese'); 75 assert.equal(getLanguageName('KR'), 'Korean'); 76 assert.equal(getLanguageName('CN'), 'Chinese'); 77 assert.equal(getLanguageName('ES'), 'Spanish'); 78 assert.equal(getLanguageName('IT'), 'Italian'); 79 assert.equal(getLanguageName('PL'), 'Polish'); 80 assert.equal(getLanguageName('NL'), 'Dutch'); 81 assert.equal(getLanguageName('DK'), 'Danish'); 82 assert.equal(getLanguageName('NO'), 'Norwegian'); 83 assert.equal(getLanguageName('SE'), 'Swedish'); 84 assert.equal(getLanguageName('ID'), 'Indonesian'); 85 }); 86 87 test('returns English for unknown country', () => { 88 assert.equal(getLanguageName('XX'), 'English'); 89 }); 90 91 test('respects explicit language code override', () => { 92 // DE country, but override with French 93 assert.equal(getLanguageName('DE', 'fr'), 'French'); 94 assert.equal(getLanguageName('AU', 'ja'), 'Japanese'); 95 assert.equal(getLanguageName('AU', 'hi'), 'Hindi'); 96 assert.equal(getLanguageName('AU', 'zh-CN'), 'Chinese'); 97 }); 98 99 test('falls back to country name for unknown language code override', () => { 100 // 'xyz' not in languageMap, falls back to LANGUAGE_NAMES[countryCode] 101 assert.equal(getLanguageName('AU', 'xyz'), 'English'); 102 }); 103 }); 104 105 describe('filterKeywordsByVolume', () => { 106 test('filters keywords by minimum volume and sorts descending', async () => { 107 const csvPath = '/tmp/kv-filter-test.csv'; 108 const outPath = '/tmp/kv-filter-out.txt'; 109 110 fs.writeFileSync( 111 csvPath, 112 [ 113 'keyword,search_volume,competition,cpc_low,cpc_high,related_to,country_code', 114 'plumber,5000,0.5,1.0,5.0,plumber,AU', 115 'electrician,1000,0.3,0.5,3.0,electrician,AU', 116 'roofer,100,0.2,0.2,1.0,roofer,AU', 117 ].join('\n') 118 ); 119 120 try { 121 const stats = await filterKeywordsByVolume(csvPath, 1000, outPath); 122 assert.equal(stats.totalKeywords, 3); 123 assert.equal(stats.filteredKeywords, 2); 124 assert.equal(stats.removedKeywords, 1); 125 126 const content = fs.readFileSync(outPath, 'utf-8'); 127 const lines = content.trim().split('\n'); 128 assert.equal(lines[0], 'plumber'); // Highest volume first 129 assert.equal(lines[1], 'electrician'); 130 assert.ok(!content.includes('roofer')); 131 } finally { 132 if (fs.existsSync(csvPath)) fs.unlinkSync(csvPath); 133 if (fs.existsSync(outPath)) fs.unlinkSync(outPath); 134 } 135 }); 136 137 test('handles empty CSV (no data rows)', async () => { 138 const csvPath = '/tmp/kv-filter-empty.csv'; 139 const outPath = '/tmp/kv-filter-empty-out.txt'; 140 141 fs.writeFileSync( 142 csvPath, 143 'keyword,search_volume,competition,cpc_low,cpc_high,related_to,country_code\n' 144 ); 145 146 try { 147 const stats = await filterKeywordsByVolume(csvPath, 100, outPath); 148 assert.equal(stats.totalKeywords, 0); 149 assert.equal(stats.filteredKeywords, 0); 150 assert.equal(stats.removedKeywords, 0); 151 } finally { 152 if (fs.existsSync(csvPath)) fs.unlinkSync(csvPath); 153 if (fs.existsSync(outPath)) fs.unlinkSync(outPath); 154 } 155 }); 156 157 test('keeps all when threshold is 0', async () => { 158 const csvPath = '/tmp/kv-filter-zero.csv'; 159 const outPath = '/tmp/kv-filter-zero-out.txt'; 160 161 fs.writeFileSync( 162 csvPath, 163 [ 164 'keyword,search_volume,competition,cpc_low,cpc_high,related_to,country_code', 165 'a,500,0.5,1,5,a,AU', 166 'b,200,0.3,0.5,3,b,AU', 167 ].join('\n') 168 ); 169 170 try { 171 const stats = await filterKeywordsByVolume(csvPath, 0, outPath); 172 assert.equal(stats.filteredKeywords, 2); 173 assert.equal(stats.removedKeywords, 0); 174 } finally { 175 if (fs.existsSync(csvPath)) fs.unlinkSync(csvPath); 176 if (fs.existsSync(outPath)) fs.unlinkSync(outPath); 177 } 178 }); 179 }); 180 181 describe('analyzeSearchVolumes - empty CSV', () => { 182 test('returns empty structure for CSV with no data rows', async () => { 183 const csvPath = '/tmp/kv-analyze-empty.csv'; 184 fs.writeFileSync( 185 csvPath, 186 'keyword,search_volume,competition,cpc_low,cpc_high,related_to,country_code\n' 187 ); 188 189 try { 190 const stats = await analyzeSearchVolumes(csvPath); 191 assert.equal(stats.total_keywords, 0); 192 assert.deepEqual(stats.statistics, {}); 193 assert.deepEqual(stats.distribution, {}); 194 assert.deepEqual(stats.recommendations, {}); 195 } finally { 196 if (fs.existsSync(csvPath)) fs.unlinkSync(csvPath); 197 } 198 }); 199 }); 200 201 describe('generateSearchVolumeCSV - file not found', () => { 202 test('throws when input file does not exist', async () => { 203 await assert.rejects( 204 () => generateSearchVolumeCSV('/tmp/nonexistent-99999.txt', 'AU', '/tmp/out.csv'), 205 /File not found/ 206 ); 207 }); 208 });