/ tests / utils / keyword-validator-supplement.test.js
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  });