compact-contacts.test.js
1 /** 2 * Compact Contacts Tests 3 * 4 * Tests for compactContacts: deduplication of emails, phones, social profiles, 5 * removal of empty/null values, and edge cases. 6 * Pure function — no external dependencies. 7 */ 8 9 import { test, describe } from 'node:test'; 10 import assert from 'node:assert/strict'; 11 12 import { compactContacts } from '../../src/utils/compact-contacts.js'; 13 14 // ─── null / edge input handling ────────────────────────────────────────────── 15 16 describe('compactContacts — edge inputs', () => { 17 test('returns null for null input', () => { 18 assert.equal(compactContacts(null), null); 19 }); 20 21 test('returns undefined for undefined input', () => { 22 assert.equal(compactContacts(undefined), undefined); 23 }); 24 25 test('returns array input unchanged (not an object)', () => { 26 const arr = [1, 2, 3]; 27 assert.equal(compactContacts(arr), arr); 28 }); 29 30 test('returns non-object types unchanged', () => { 31 assert.equal(compactContacts(42), 42); 32 assert.equal(compactContacts('string'), 'string'); 33 assert.equal(compactContacts(true), true); 34 }); 35 36 test('returns new object (does not mutate original)', () => { 37 const input = { business_name: 'Test' }; 38 const result = compactContacts(input); 39 assert.notEqual(result, input); 40 assert.deepStrictEqual(result, { business_name: 'Test' }); 41 }); 42 }); 43 44 // ─── email deduplication ───────────────────────────────────────────────────── 45 46 describe('compactContacts — email deduplication', () => { 47 test('keeps unique emails', () => { 48 const input = { 49 email_addresses: [ 50 { email: 'info@example.com', source: 'page' }, 51 { email: 'sales@example.com', source: 'footer' }, 52 ], 53 }; 54 const result = compactContacts(input); 55 assert.equal(result.email_addresses.length, 2); 56 }); 57 58 test('deduplicates emails case-insensitively', () => { 59 const input = { 60 email_addresses: [ 61 { email: 'Info@Example.COM', source: 'header' }, 62 { email: 'info@example.com', source: 'footer' }, 63 ], 64 }; 65 const result = compactContacts(input); 66 assert.equal(result.email_addresses.length, 1); 67 assert.equal(result.email_addresses[0].email, 'Info@Example.COM'); // keeps first 68 }); 69 70 test('deduplicates emails with extra whitespace', () => { 71 const input = { 72 email_addresses: [ 73 { email: ' info@example.com ', source: 'page' }, 74 { email: 'info@example.com', source: 'footer' }, 75 ], 76 }; 77 const result = compactContacts(input); 78 assert.equal(result.email_addresses.length, 1); 79 }); 80 81 test('supports address field as fallback to email field', () => { 82 const input = { 83 email_addresses: [ 84 { address: 'info@example.com' }, 85 { email: 'info@example.com' }, 86 ], 87 }; 88 const result = compactContacts(input); 89 assert.equal(result.email_addresses.length, 1); 90 }); 91 92 test('filters out null entries', () => { 93 const input = { 94 email_addresses: [null, { email: 'ok@test.com' }, undefined], 95 }; 96 const result = compactContacts(input); 97 assert.equal(result.email_addresses.length, 1); 98 }); 99 100 test('filters out non-object entries', () => { 101 const input = { 102 email_addresses: ['info@example.com', 42, { email: 'ok@test.com' }], 103 }; 104 const result = compactContacts(input); 105 assert.equal(result.email_addresses.length, 1); 106 assert.equal(result.email_addresses[0].email, 'ok@test.com'); 107 }); 108 109 test('filters out entries with empty email', () => { 110 const input = { 111 email_addresses: [ 112 { email: '' }, 113 { email: 'valid@test.com' }, 114 ], 115 }; 116 const result = compactContacts(input); 117 assert.equal(result.email_addresses.length, 1); 118 }); 119 120 test('removes email_addresses key if all entries filtered out', () => { 121 const input = { 122 email_addresses: [null, { email: '' }], 123 }; 124 const result = compactContacts(input); 125 assert.equal(result.email_addresses, undefined); 126 }); 127 }); 128 129 // ─── phone deduplication ───────────────────────────────────────────────────── 130 131 describe('compactContacts — phone deduplication', () => { 132 test('keeps unique phone numbers', () => { 133 const input = { 134 phone_numbers: [ 135 { number: '+61 400 000 001', type: 'mobile' }, 136 { number: '+61 400 000 002', type: 'mobile' }, 137 ], 138 }; 139 const result = compactContacts(input); 140 assert.equal(result.phone_numbers.length, 2); 141 }); 142 143 test('deduplicates phone numbers ignoring whitespace', () => { 144 const input = { 145 phone_numbers: [ 146 { number: '+61 400 000 001', type: 'mobile' }, 147 { number: '+61400000001', type: 'office' }, 148 ], 149 }; 150 const result = compactContacts(input); 151 assert.equal(result.phone_numbers.length, 1); 152 assert.equal(result.phone_numbers[0].number, '+61 400 000 001'); // keeps first 153 }); 154 155 test('filters out null/undefined entries', () => { 156 const input = { 157 phone_numbers: [null, { number: '+61400000001' }, undefined], 158 }; 159 const result = compactContacts(input); 160 assert.equal(result.phone_numbers.length, 1); 161 }); 162 163 test('filters out entries with empty number', () => { 164 const input = { 165 phone_numbers: [ 166 { number: '' }, 167 { number: '+61400000001' }, 168 ], 169 }; 170 const result = compactContacts(input); 171 assert.equal(result.phone_numbers.length, 1); 172 }); 173 174 test('filters out non-object entries', () => { 175 const input = { 176 phone_numbers: ['0400000001', { number: '+61400000001' }], 177 }; 178 const result = compactContacts(input); 179 assert.equal(result.phone_numbers.length, 1); 180 }); 181 182 test('removes phone_numbers key if all entries filtered out', () => { 183 const input = { 184 phone_numbers: [null, { number: '' }], 185 }; 186 const result = compactContacts(input); 187 assert.equal(result.phone_numbers, undefined); 188 }); 189 }); 190 191 // ─── social profile deduplication ──────────────────────────────────────────── 192 193 describe('compactContacts — social profile deduplication', () => { 194 test('keeps unique social profiles', () => { 195 const input = { 196 social_profiles: [ 197 { url: 'https://facebook.com/biz', platform: 'facebook' }, 198 { url: 'https://twitter.com/biz', platform: 'twitter' }, 199 ], 200 }; 201 const result = compactContacts(input); 202 assert.equal(result.social_profiles.length, 2); 203 }); 204 205 test('deduplicates social profiles by URL', () => { 206 const input = { 207 social_profiles: [ 208 { url: 'https://facebook.com/biz', platform: 'facebook' }, 209 { url: 'https://facebook.com/biz', platform: 'fb' }, 210 ], 211 }; 212 const result = compactContacts(input); 213 assert.equal(result.social_profiles.length, 1); 214 assert.equal(result.social_profiles[0].platform, 'facebook'); // keeps first 215 }); 216 217 test('trims URL whitespace for dedup', () => { 218 const input = { 219 social_profiles: [ 220 { url: ' https://facebook.com/biz ' }, 221 { url: 'https://facebook.com/biz' }, 222 ], 223 }; 224 const result = compactContacts(input); 225 assert.equal(result.social_profiles.length, 1); 226 }); 227 228 test('filters out entries with empty URL', () => { 229 const input = { 230 social_profiles: [ 231 { url: '' }, 232 { url: 'https://facebook.com/biz' }, 233 ], 234 }; 235 const result = compactContacts(input); 236 assert.equal(result.social_profiles.length, 1); 237 }); 238 239 test('filters out null entries', () => { 240 const input = { 241 social_profiles: [null, { url: 'https://facebook.com/biz' }], 242 }; 243 const result = compactContacts(input); 244 assert.equal(result.social_profiles.length, 1); 245 }); 246 247 test('removes social_profiles key if all entries filtered out', () => { 248 const input = { 249 social_profiles: [null, { url: '' }], 250 }; 251 const result = compactContacts(input); 252 assert.equal(result.social_profiles, undefined); 253 }); 254 }); 255 256 // ─── empty value cleanup ───────────────────────────────────────────────────── 257 258 describe('compactContacts — empty value cleanup', () => { 259 test('removes null values', () => { 260 const input = { business_name: null, website: 'https://test.com' }; 261 const result = compactContacts(input); 262 assert.equal(result.business_name, undefined); 263 assert.equal(result.website, 'https://test.com'); 264 }); 265 266 test('removes empty string values', () => { 267 const input = { business_name: '', website: 'https://test.com' }; 268 const result = compactContacts(input); 269 assert.equal(result.business_name, undefined); 270 }); 271 272 test('removes empty array values', () => { 273 const input = { email_addresses: [], website: 'https://test.com' }; 274 const result = compactContacts(input); 275 assert.equal(result.email_addresses, undefined); 276 }); 277 278 test('keeps zero as a value', () => { 279 const input = { count: 0 }; 280 const result = compactContacts(input); 281 assert.equal(result.count, 0); 282 }); 283 284 test('keeps false as a value', () => { 285 const input = { verified: false }; 286 const result = compactContacts(input); 287 assert.equal(result.verified, false); 288 }); 289 290 test('keeps non-empty arrays', () => { 291 const input = { 292 email_addresses: [{ email: 'test@test.com' }], 293 }; 294 const result = compactContacts(input); 295 assert.equal(result.email_addresses.length, 1); 296 }); 297 298 test('keeps non-empty strings', () => { 299 const input = { business_name: 'Acme Corp' }; 300 const result = compactContacts(input); 301 assert.equal(result.business_name, 'Acme Corp'); 302 }); 303 }); 304 305 // ─── full integration scenario ─────────────────────────────────────────────── 306 307 describe('compactContacts — full integration', () => { 308 test('handles a realistic contacts object with all dedup types', () => { 309 const input = { 310 email_addresses: [ 311 { email: 'info@acme.com', source: 'header' }, 312 { email: 'INFO@ACME.COM', source: 'footer' }, 313 { email: 'sales@acme.com', source: 'contact' }, 314 null, 315 ], 316 phone_numbers: [ 317 { number: '+61 2 9999 0000', type: 'landline' }, 318 { number: '+6129999 0000', type: 'office' }, 319 ], 320 social_profiles: [ 321 { url: 'https://facebook.com/acme', platform: 'facebook' }, 322 { url: 'https://facebook.com/acme', platform: 'fb' }, 323 { url: 'https://linkedin.com/company/acme', platform: 'linkedin' }, 324 ], 325 business_name: 'Acme Corp', 326 notes: '', 327 archived: null, 328 }; 329 const result = compactContacts(input); 330 331 // Deduped correctly 332 assert.equal(result.email_addresses.length, 2); // info + sales 333 assert.equal(result.phone_numbers.length, 1); // whitespace dedup 334 assert.equal(result.social_profiles.length, 2); // fb dedup, linkedin kept 335 336 // Empty values cleaned 337 assert.equal(result.notes, undefined); 338 assert.equal(result.archived, undefined); 339 340 // Non-empty preserved 341 assert.equal(result.business_name, 'Acme Corp'); 342 }); 343 344 test('returns empty object when all fields are null/empty', () => { 345 const input = { 346 email_addresses: [], 347 phone_numbers: [], 348 social_profiles: [], 349 notes: null, 350 tags: '', 351 }; 352 const result = compactContacts(input); 353 assert.deepStrictEqual(result, {}); 354 }); 355 });