diff --git a/README.md b/README.md index e1c8aa8..872acea 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,48 @@ Options can be set as attributes in the `` tag. | `data-phonetic` | `true` | Add phonetic information for screen readers. | | `data-search` | `false` | Find and mark up untagged call signs in the document. | +# Testing +This project includes comprehensive unit tests using Jest with a primary focus on regex pattern validation. + +## Running Tests +```bash +# Install dependencies +npm install + +# Run all tests +npm test + +# Run linting +npm run lint +``` + +## Test Coverage (71 tests total) +The test suite focuses primarily on validating the two core regex patterns that drive the library's functionality: + +### 1. **SEARCH_REGEX Pattern Tests** (`tests/searchCallsigns.test.js`) +Tests the regex pattern `/([A-Z,\d]{1,3}\d[A-Z]{1,3}(?:\/\d)?)\s/` that detects call signs in text: +- Valid call sign pattern matching (single/double/triple letter prefixes) +- Portable indicator detection (`/3`, `/5`, etc.) +- Edge cases and boundary conditions +- Invalid pattern rejection (no trailing space, wrong format, etc.) +- Real-world call sign examples from multiple countries +- Whitespace handling and greedy matching behavior + +### 2. **PARTS_REGEX Pattern Tests** (`tests/partsRegex.test.js`) +Tests the regex pattern `/([A-Z,\d]{1,3})(\d)([A-Z]{1,3})(?:\/(\d))?/` that parses call signs into components: +- Prefix parsing (1-3 characters: W, SM, VK2, etc.) +- Area digit extraction (0-9) +- Suffix parsing (1-3 letters: A, AB, ABC) +- Portable indicator capture group +- Greedy matching behavior with long prefixes +- Component extraction from embedded text + +### 3. **Supporting Method Tests** +- `tests/getFlag.test.js` - ISO code to Unicode flag conversion (used after PREFIX_TABLE matching) +- `tests/getPhonetics.test.js` - Phonetic alphabet mapping for regex-parsed call signs + +Test files are located in the `tests/` directory with clear documentation of each regex pattern's behavior and edge cases. + # Minification The files are intentionally not provided [minified](https://en.wikipedia.org/wiki/Minification_(programming)). Amateur radio is about learning and experimenting. diff --git a/package.json b/package.json index 81b9c1a..ec11c1b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,16 @@ { "type": "module", + "scripts": { + "test": "jest", + "lint": "eslint src/" + }, "devDependencies": { - "eslint": "^9.4.0" + "eslint": "^9.4.0", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0" + }, + "jest": { + "testEnvironment": "jsdom", + "transform": {} } } diff --git a/tests/getFlag.test.js b/tests/getFlag.test.js new file mode 100644 index 0000000..96e397a --- /dev/null +++ b/tests/getFlag.test.js @@ -0,0 +1,44 @@ +/** + * Unit tests for the getFlag method + * Validates the conversion of ISO country codes extracted by PREFIX_TABLE regex matching + * to Unicode Regional Indicator Symbols (emoji flags) + */ + +/** + * Converts an ISO country code to a Unicode Regional Indicator Symbol (emoji flag). + * @param {!string} code The ISO 3166-1 alpha-2 code + * @returns {string} + */ +function getFlag(code) { + 'use strict'; + return String.fromCodePoint(...[...code].map(c => c.charCodeAt() + 127397)); +} + +describe('getFlag method - ISO code to emoji conversion', () => { + test('should correctly convert common country codes from PREFIX_TABLE', () => { + // Test codes that would be matched from PREFIX_TABLE after regex parsing + expect(getFlag('US')).toBe(String.fromCodePoint(127482, 127480)); // πŸ‡ΊπŸ‡Έ + expect(getFlag('SE')).toBe(String.fromCodePoint(127480, 127466)); // πŸ‡ΈπŸ‡ͺ + expect(getFlag('DE')).toBe(String.fromCodePoint(127465, 127466)); // πŸ‡©πŸ‡ͺ + expect(getFlag('GB')).toBe(String.fromCodePoint(127468, 127463)); // πŸ‡¬πŸ‡§ + expect(getFlag('JP')).toBe(String.fromCodePoint(127471, 127477)); // πŸ‡―πŸ‡΅ + expect(getFlag('CA')).toBe(String.fromCodePoint(127464, 127462)); // πŸ‡¨πŸ‡¦ + }); + + test('should apply correct mathematical transformation (charCode + 127397)', () => { + // A = 65, 65 + 127397 = 127462 (Regional Indicator A) + // Z = 90, 90 + 127397 = 127487 (Regional Indicator Z) + expect(getFlag('AA')).toBe(String.fromCodePoint(127462, 127462)); + expect(getFlag('ZZ')).toBe(String.fromCodePoint(127487, 127487)); + }); + + test('should handle all ISO codes from PREFIX_TABLE entries', () => { + // Test a sample of codes that appear in the PREFIX_TABLE + const prefixCodes = ['AU', 'BR', 'FR', 'IT', 'MX', 'ES', 'CN', 'IN']; + prefixCodes.forEach(code => { + const result = getFlag(code); + expect(result).toBeTruthy(); + expect(result.length).toBe(4); // Each emoji flag is 2 code points (4 bytes in UTF-16) + }); + }); +}); \ No newline at end of file diff --git a/tests/getPhonetics.test.js b/tests/getPhonetics.test.js new file mode 100644 index 0000000..639f11d --- /dev/null +++ b/tests/getPhonetics.test.js @@ -0,0 +1,137 @@ +/** + * Unit tests for the getPhonetics method + * Tests phonetic alphabet mapping for characters extracted from call signs via PARTS_REGEX + */ + +// Copy the phonetic table and getPhonetics method for testing +const PHONETIC_TABLE = new Map([ + ['A', 'Alfa'], + ['B', 'Bravo'], + ['C', 'Charlie'], + ['D', 'Delta'], + ['E', 'Echo'], + ['F', 'Foxtrot'], + ['G', 'Golf'], + ['H', 'Hotel'], + ['I', 'India'], + ['J', 'Juliett'], + ['K', 'Kilo'], + ['L', 'Lima'], + ['M', 'Mike'], + ['N', 'November'], + ['O', 'Oscar'], + ['P', 'Papa'], + ['Q', 'Quebec'], + ['R', 'Romeo'], + ['S', 'Sierra'], + ['T', 'Tango'], + ['U', 'Uniform'], + ['V', 'Victor'], + ['W', 'Whiskey'], + ['X', 'X-ray'], + ['Y', 'Yankee'], + ['Z', 'Zulu'], + ['0', 'Ziro'], + ['1', 'One'], + ['2', 'Two'], + ['3', 'Tree'], + ['4', 'Four'], + ['5', 'Five'], + ['6', 'Six'], + ['7', 'Seven'], + ['8', 'Eight'], + ['9', 'Niner'], +]); + +/** + * @param {string} letters The string of letters to expand + * @returns {string} + */ +function getPhonetics(letters) { + 'use strict'; + let ret = ""; + for (var i = 0; i < letters.length; i++) { + ret += PHONETIC_TABLE.get(letters.charAt(i)) + " "; + } + return ret.slice(0, -1); +} + +describe('getPhonetics method - phonetic mapping for regex-parsed call signs', () => { + describe('Complete call sign phonetic conversion', () => { + test('should convert call signs matched by SEARCH_REGEX', () => { + // These would be matched by SEARCH_REGEX and parsed by PARTS_REGEX + expect(getPhonetics('W1AW')).toBe('Whiskey One Alfa Whiskey'); + expect(getPhonetics('K2ABC')).toBe('Kilo Two Alfa Bravo Charlie'); + expect(getPhonetics('SM8AYA')).toBe('Sierra Mike Eight Alfa Yankee Alfa'); + expect(getPhonetics('DL1ABC')).toBe('Delta Lima One Alfa Bravo Charlie'); + }); + + test('should handle all alphabet characters A-Z from PHONETIC_TABLE', () => { + const tests = [ + ['A', 'Alfa'], ['B', 'Bravo'], ['C', 'Charlie'], ['D', 'Delta'], + ['E', 'Echo'], ['F', 'Foxtrot'], ['G', 'Golf'], ['H', 'Hotel'], + ['I', 'India'], ['J', 'Juliett'], ['K', 'Kilo'], ['L', 'Lima'], + ['M', 'Mike'], ['N', 'November'], ['O', 'Oscar'], ['P', 'Papa'], + ['Q', 'Quebec'], ['R', 'Romeo'], ['S', 'Sierra'], ['T', 'Tango'], + ['U', 'Uniform'], ['V', 'Victor'], ['W', 'Whiskey'], ['X', 'X-ray'], + ['Y', 'Yankee'], ['Z', 'Zulu'] + ]; + + tests.forEach(([char, phonetic]) => { + expect(getPhonetics(char)).toBe(phonetic); + }); + }); + + test('should handle all digits 0-9 from PHONETIC_TABLE', () => { + const tests = [ + ['0', 'Ziro'], ['1', 'One'], ['2', 'Two'], ['3', 'Tree'], + ['4', 'Four'], ['5', 'Five'], ['6', 'Six'], ['7', 'Seven'], + ['8', 'Eight'], ['9', 'Niner'] + ]; + + tests.forEach(([digit, phonetic]) => { + expect(getPhonetics(digit)).toBe(phonetic); + }); + }); + }); + + describe('Edge cases and regex-related scenarios', () => { + test('should handle empty string (no regex match)', () => { + expect(getPhonetics('')).toBe(''); + }); + + test('should handle characters not in PHONETIC_TABLE (like / in portable indicators)', () => { + // Slash in W1ABC/3 is not in the phonetic table + const result = getPhonetics('W1ABC/3'); + expect(result).toContain('undefined'); // Will have undefined for / + }); + + test('should correctly process minimum length call signs (PARTS_REGEX captures)', () => { + // W1A is minimum valid call sign pattern + expect(getPhonetics('W1A')).toBe('Whiskey One Alfa'); + }); + + test('should correctly process maximum length call signs', () => { + // ABC1XYZ is maximum length pattern (3+1+3) + expect(getPhonetics('ABC1XYZ')).toBe('Alfa Bravo Charlie One X-ray Yankee Zulu'); + }); + }); + + describe('Real-world regex-matched patterns', () => { + test('should handle common US prefixes matched by PREFIX_TABLE', () => { + // US prefixes: W, K, N, AA-AL, etc. + expect(getPhonetics('W')).toBe('Whiskey'); + expect(getPhonetics('K')).toBe('Kilo'); + expect(getPhonetics('N')).toBe('November'); + expect(getPhonetics('AA')).toBe('Alfa Alfa'); + }); + + test('should handle international prefixes from PREFIX_TABLE', () => { + // Common international prefixes + expect(getPhonetics('SM')).toBe('Sierra Mike'); // Sweden + expect(getPhonetics('DL')).toBe('Delta Lima'); // Germany + expect(getPhonetics('JA')).toBe('Juliett Alfa'); // Japan + expect(getPhonetics('VK')).toBe('Victor Kilo'); // Australia + }); + }); +}); \ No newline at end of file diff --git a/tests/partsRegex.test.js b/tests/partsRegex.test.js new file mode 100644 index 0000000..82e4251 --- /dev/null +++ b/tests/partsRegex.test.js @@ -0,0 +1,301 @@ +/** + * Unit tests focusing on PARTS_REGEX pattern validation + * The PARTS_REGEX pattern: /([A-Z,\d]{1,3})(\d)([A-Z]{1,3})(?:\/(\d))?/ + * This pattern parses call signs into their components: + * - Capture group 1: Prefix (1-3 alphanumeric characters) + * - Capture group 2: Area digit (exactly 1 digit) + * - Capture group 3: Suffix (1-3 letters) + * - Capture group 4: Portable indicator digit (optional) + */ + +const PARTS_REGEX = /([A-Z,\d]{1,3})(\d)([A-Z]{1,3})(?:\/(\d))?/; + +describe('PARTS_REGEX pattern validation', () => { + describe('Basic parsing of call sign components', () => { + test('should parse single-letter prefix call sign', () => { + const match = 'W1AW'.match(PARTS_REGEX); + expect(match).toBeTruthy(); + expect(match[1]).toBe('W'); // prefix + expect(match[2]).toBe('1'); // area digit + expect(match[3]).toBe('AW'); // suffix + expect(match[4]).toBeUndefined(); // no portable indicator + }); + + test('should parse two-letter prefix call sign', () => { + const match = 'SM8AYA'.match(PARTS_REGEX); + expect(match).toBeTruthy(); + expect(match[1]).toBe('SM'); // prefix + expect(match[2]).toBe('8'); // area digit + expect(match[3]).toBe('AYA'); // suffix + expect(match[4]).toBeUndefined(); + }); + + test('should parse three-letter prefix call sign', () => { + const match = 'VK2ABC'.match(PARTS_REGEX); + expect(match).toBeTruthy(); + expect(match[1]).toBe('VK'); // prefix (will match first 2 chars) + expect(match[2]).toBe('2'); // area digit + expect(match[3]).toBe('ABC'); // suffix + }); + + test('should parse call sign with portable indicator', () => { + const match = 'W1ABC/3'.match(PARTS_REGEX); + expect(match).toBeTruthy(); + expect(match[1]).toBe('W'); // prefix + expect(match[2]).toBe('1'); // area digit + expect(match[3]).toBe('ABC'); // suffix + expect(match[4]).toBe('3'); // portable indicator + }); + }); + + describe('Prefix variations (1-3 characters)', () => { + test('should parse minimum prefix length (1 character)', () => { + const match = 'K2ABC'.match(PARTS_REGEX); + expect(match[1]).toBe('K'); + expect(match[2]).toBe('2'); + expect(match[3]).toBe('ABC'); + }); + + test('should parse medium prefix length (2 characters)', () => { + const match = 'DL1ABC'.match(PARTS_REGEX); + expect(match[1]).toBe('DL'); + expect(match[2]).toBe('1'); + expect(match[3]).toBe('ABC'); + }); + + test('should parse maximum prefix length (3 characters)', () => { + const match = 'ABC1XYZ'.match(PARTS_REGEX); + expect(match[1]).toBe('ABC'); + expect(match[2]).toBe('1'); + expect(match[3]).toBe('XYZ'); + }); + + test('should parse prefix with number', () => { + const match = '9V1ABC'.match(PARTS_REGEX); + expect(match[1]).toBe('9V'); + expect(match[2]).toBe('1'); + expect(match[3]).toBe('ABC'); + }); + + test('should parse prefix with leading number', () => { + const match = '3D2XYZ'.match(PARTS_REGEX); + expect(match[1]).toBe('3D'); + expect(match[2]).toBe('2'); + expect(match[3]).toBe('XYZ'); + }); + }); + + describe('Area digit variations (0-9)', () => { + test('should parse all digits 0-9 in area position', () => { + for (let i = 0; i < 10; i++) { + const callsign = `W${i}AW`; + const match = callsign.match(PARTS_REGEX); + expect(match).toBeTruthy(); + expect(match[2]).toBe(String(i)); + } + }); + + test('should parse area digit 0', () => { + const match = 'G0ABC'.match(PARTS_REGEX); + expect(match[2]).toBe('0'); + }); + + test('should parse area digit 9', () => { + const match = 'K9XYZ'.match(PARTS_REGEX); + expect(match[2]).toBe('9'); + }); + }); + + describe('Suffix variations (1-3 letters)', () => { + test('should parse minimum suffix length (1 letter)', () => { + const match = 'W1A'.match(PARTS_REGEX); + expect(match[1]).toBe('W'); + expect(match[2]).toBe('1'); + expect(match[3]).toBe('A'); + }); + + test('should parse medium suffix length (2 letters)', () => { + const match = 'W1AB'.match(PARTS_REGEX); + expect(match[1]).toBe('W'); + expect(match[2]).toBe('1'); + expect(match[3]).toBe('AB'); + }); + + test('should parse maximum suffix length (3 letters)', () => { + const match = 'W1ABC'.match(PARTS_REGEX); + expect(match[1]).toBe('W'); + expect(match[2]).toBe('1'); + expect(match[3]).toBe('ABC'); + }); + }); + + describe('Portable indicator parsing', () => { + test('should parse all portable indicators (0-9)', () => { + for (let i = 0; i < 10; i++) { + const callsign = `W1ABC/${i}`; + const match = callsign.match(PARTS_REGEX); + expect(match).toBeTruthy(); + expect(match[4]).toBe(String(i)); + } + }); + + test('should parse portable indicator 0', () => { + const match = 'W1ABC/0'.match(PARTS_REGEX); + expect(match[4]).toBe('0'); + }); + + test('should parse portable indicator 9', () => { + const match = 'W1ABC/9'.match(PARTS_REGEX); + expect(match[4]).toBe('9'); + }); + + test('should handle missing portable indicator', () => { + const match = 'W1ABC'.match(PARTS_REGEX); + expect(match[4]).toBeUndefined(); + }); + }); + + describe('Edge cases and special patterns', () => { + test('should parse minimum length call sign (1+1+1)', () => { + const match = 'W1A'.match(PARTS_REGEX); + expect(match[0]).toBe('W1A'); + expect(match[1]).toBe('W'); + expect(match[2]).toBe('1'); + expect(match[3]).toBe('A'); + }); + + test('should parse maximum length call sign (3+1+3+/+1)', () => { + const match = 'ABC1XYZ/5'.match(PARTS_REGEX); + expect(match[0]).toBe('ABC1XYZ/5'); + expect(match[1]).toBe('ABC'); + expect(match[2]).toBe('1'); + expect(match[3]).toBe('XYZ'); + expect(match[4]).toBe('5'); + }); + + test('should parse call sign with comma in prefix', () => { + // The regex allows comma: [A-Z,\d] + const match = 'W,1ABC'.match(PARTS_REGEX); + expect(match[1]).toBe('W,'); + expect(match[2]).toBe('1'); + expect(match[3]).toBe('ABC'); + }); + + test('should parse call sign when embedded in text', () => { + const text = 'Contact W1AW today'; + const match = text.match(PARTS_REGEX); + expect(match).toBeTruthy(); + expect(match[1]).toBe('W'); + expect(match[2]).toBe('1'); + expect(match[3]).toBe('AW'); + }); + }); + + describe('Invalid patterns (should match but with unexpected results)', () => { + test('should not parse lowercase letters correctly', () => { + // Lowercase won't match the [A-Z] pattern + const match = 'w1aw'.match(PARTS_REGEX); + expect(match).toBeNull(); + }); + + test('should not parse patterns with no area digit', () => { + const match = 'WABC'.match(PARTS_REGEX); + expect(match).toBeNull(); + }); + + test('should capture from long prefix correctly (greedy matching)', () => { + // The regex {1,3} is greedy, so ABCD1XYZ will match BCD1XYZ (last 3 chars + digit + suffix) + const match = 'ABCD1XYZ'.match(PARTS_REGEX); + expect(match).toBeTruthy(); + expect(match[1]).toBe('BCD'); // Captures last 3 characters before digit + expect(match[2]).toBe('1'); + expect(match[3]).toBe('XYZ'); + }); + + test('should not parse special characters', () => { + const match = 'W-1ABC'.match(PARTS_REGEX); + expect(match).toBeNull(); + }); + }); + + describe('Real-world call sign parsing', () => { + test('should correctly parse common US call signs', () => { + const usCallSigns = [ + { call: 'W1AW', prefix: 'W', digit: '1', suffix: 'AW' }, + { call: 'K2ABC', prefix: 'K', digit: '2', suffix: 'ABC' }, + { call: 'N3XYZ', prefix: 'N', digit: '3', suffix: 'XYZ' }, + { call: 'AA1AA', prefix: 'AA', digit: '1', suffix: 'AA' }, + { call: 'KD8ABC', prefix: 'KD', digit: '8', suffix: 'ABC' }, + ]; + + usCallSigns.forEach(({ call, prefix, digit, suffix }) => { + const match = call.match(PARTS_REGEX); + expect(match[1]).toBe(prefix); + expect(match[2]).toBe(digit); + expect(match[3]).toBe(suffix); + }); + }); + + test('should correctly parse common international call signs', () => { + const intlCallSigns = [ + { call: 'SM8AYA', prefix: 'SM', digit: '8', suffix: 'AYA' }, + { call: 'DL1ABC', prefix: 'DL', digit: '1', suffix: 'ABC' }, + { call: 'G0ABC', prefix: 'G', digit: '0', suffix: 'ABC' }, + { call: 'JA1XYZ', prefix: 'JA', digit: '1', suffix: 'XYZ' }, + { call: 'VK2DEF', prefix: 'VK', digit: '2', suffix: 'DEF' }, + ]; + + intlCallSigns.forEach(({ call, prefix, digit, suffix }) => { + const match = call.match(PARTS_REGEX); + expect(match[1]).toBe(prefix); + expect(match[2]).toBe(digit); + expect(match[3]).toBe(suffix); + }); + }); + + test('should correctly parse call signs with portable indicators', () => { + const portableCallSigns = [ + { call: 'W1ABC/3', prefix: 'W', digit: '1', suffix: 'ABC', portable: '3' }, + { call: 'SM8AYA/5', prefix: 'SM', digit: '8', suffix: 'AYA', portable: '5' }, + { call: 'K2ABC/0', prefix: 'K', digit: '2', suffix: 'ABC', portable: '0' }, + ]; + + portableCallSigns.forEach(({ call, prefix, digit, suffix, portable }) => { + const match = call.match(PARTS_REGEX); + expect(match[1]).toBe(prefix); + expect(match[2]).toBe(digit); + expect(match[3]).toBe(suffix); + expect(match[4]).toBe(portable); + }); + }); + }); + + describe('Comparison with SEARCH_REGEX requirements', () => { + test('should parse same patterns that SEARCH_REGEX would match', () => { + // These are patterns that SEARCH_REGEX would match (with trailing space) + // PARTS_REGEX should parse them correctly + const validPatterns = [ + 'W1AW', + 'K2ABC', + 'SM8AYA', + 'DL1ABC', + 'W1ABC/3', + '9V1ABC', + 'VK2ABC', + ]; + + validPatterns.forEach(pattern => { + const match = pattern.match(PARTS_REGEX); + expect(match).toBeTruthy(); + expect(match[0]).toBe(pattern); + }); + }); + + test('should not require trailing space (unlike SEARCH_REGEX)', () => { + // PARTS_REGEX doesn't require space, unlike SEARCH_REGEX + const match = 'W1AW'.match(PARTS_REGEX); + expect(match).toBeTruthy(); + expect(match[0]).toBe('W1AW'); + }); + }); +}); diff --git a/tests/searchCallsigns.test.js b/tests/searchCallsigns.test.js new file mode 100644 index 0000000..6a8123c --- /dev/null +++ b/tests/searchCallsigns.test.js @@ -0,0 +1,211 @@ +/** + * Unit tests focusing on SEARCH_REGEX pattern validation + * The SEARCH_REGEX pattern: /([A-Z,\d]{1,3}\d[A-Z]{1,3}(?:\/\d)?)\s/ + * This pattern detects call signs in text that: + * - Start with 1-3 alphanumeric characters (prefix) + * - Have exactly one digit (area number) + * - Have 1-3 letters (suffix) + * - Optionally have /digit (portable indicator) + * - Must be followed by a space + */ + +const SEARCH_REGEX = /([A-Z,\d]{1,3}\d[A-Z]{1,3}(?:\/\d)?)\s/; + +describe('SEARCH_REGEX pattern validation', () => { + describe('Valid call sign patterns', () => { + test('should match single-letter prefix patterns', () => { + expect('W1AW '.match(SEARCH_REGEX)).toBeTruthy(); + expect('W1AW '.match(SEARCH_REGEX)[1]).toBe('W1AW'); + + expect('K2ABC '.match(SEARCH_REGEX)).toBeTruthy(); + expect('K2ABC '.match(SEARCH_REGEX)[1]).toBe('K2ABC'); + + expect('N3X '.match(SEARCH_REGEX)).toBeTruthy(); + expect('N3X '.match(SEARCH_REGEX)[1]).toBe('N3X'); + }); + + test('should match two-letter prefix patterns', () => { + expect('SM8AYA '.match(SEARCH_REGEX)).toBeTruthy(); + expect('SM8AYA '.match(SEARCH_REGEX)[1]).toBe('SM8AYA'); + + expect('DL1ABC '.match(SEARCH_REGEX)).toBeTruthy(); + expect('DL1ABC '.match(SEARCH_REGEX)[1]).toBe('DL1ABC'); + + expect('G0XYZ '.match(SEARCH_REGEX)).toBeTruthy(); + expect('G0XYZ '.match(SEARCH_REGEX)[1]).toBe('G0XYZ'); + }); + + test('should match three-letter prefix patterns', () => { + expect('VK2ABC '.match(SEARCH_REGEX)).toBeTruthy(); + expect('VK2ABC '.match(SEARCH_REGEX)[1]).toBe('VK2ABC'); + + expect('XX91A '.match(SEARCH_REGEX)).toBeTruthy(); + expect('XX91A '.match(SEARCH_REGEX)[1]).toBe('XX91A'); + }); + + test('should match patterns with number in prefix', () => { + expect('9V1ABC '.match(SEARCH_REGEX)).toBeTruthy(); + expect('9V1ABC '.match(SEARCH_REGEX)[1]).toBe('9V1ABC'); + + expect('3D2XYZ '.match(SEARCH_REGEX)).toBeTruthy(); + expect('3D2XYZ '.match(SEARCH_REGEX)[1]).toBe('3D2XYZ'); + }); + + test('should match patterns with varying suffix lengths (1-3 letters)', () => { + expect('W1A '.match(SEARCH_REGEX)).toBeTruthy(); + expect('W1A '.match(SEARCH_REGEX)[1]).toBe('W1A'); + + expect('W1AB '.match(SEARCH_REGEX)).toBeTruthy(); + expect('W1AB '.match(SEARCH_REGEX)[1]).toBe('W1AB'); + + expect('W1ABC '.match(SEARCH_REGEX)).toBeTruthy(); + expect('W1ABC '.match(SEARCH_REGEX)[1]).toBe('W1ABC'); + }); + + test('should match patterns with portable indicators', () => { + expect('W1ABC/3 '.match(SEARCH_REGEX)).toBeTruthy(); + expect('W1ABC/3 '.match(SEARCH_REGEX)[1]).toBe('W1ABC/3'); + + expect('SM8AYA/5 '.match(SEARCH_REGEX)).toBeTruthy(); + expect('SM8AYA/5 '.match(SEARCH_REGEX)[1]).toBe('SM8AYA/5'); + + expect('K2ABC/0 '.match(SEARCH_REGEX)).toBeTruthy(); + expect('K2ABC/0 '.match(SEARCH_REGEX)[1]).toBe('K2ABC/0'); + }); + + test('should match all digit variations (0-9) in area number position', () => { + for (let i = 0; i < 10; i++) { + const callsign = `W${i}AW `; + const match = callsign.match(SEARCH_REGEX); + expect(match).toBeTruthy(); + expect(match[1]).toBe(`W${i}AW`); + } + }); + }); + + describe('Invalid call sign patterns (should NOT match)', () => { + test('should NOT match patterns without trailing space', () => { + expect('W1AW'.match(SEARCH_REGEX)).toBeNull(); + expect('W1AWX'.match(SEARCH_REGEX)).toBeNull(); + expect('K2ABC!'.match(SEARCH_REGEX)).toBeNull(); + }); + + test('should NOT match patterns with no digit in area position', () => { + expect('WAAW '.match(SEARCH_REGEX)).toBeNull(); + expect('KAABC '.match(SEARCH_REGEX)).toBeNull(); + }); + + test('should match patterns with multiple digits (regex allows this)', () => { + // The regex actually DOES match W12AW because {1,3} in prefix can capture W1, then 2 becomes the digit + const match = 'W12AW '.match(SEARCH_REGEX); + expect(match).toBeTruthy(); + expect(match[1]).toBe('W12AW'); // Captures W1 as prefix, 2 as digit, AW as suffix + }); + + test('should NOT match patterns with suffix too short (0 letters)', () => { + expect('W1 '.match(SEARCH_REGEX)).toBeNull(); + expect('K2 '.match(SEARCH_REGEX)).toBeNull(); + }); + + test('should NOT match patterns with suffix too long (4+ letters)', () => { + expect('W1ABCD '.match(SEARCH_REGEX)).toBeNull(); + expect('K2ABCDE '.match(SEARCH_REGEX)).toBeNull(); + }); + + test('should match patterns with long prefixes (regex captures first 3 chars)', () => { + // ABCD1ABC will match as ABC (prefix) + D1A (but wait, the regex will actually match BCD1ABC) + // The regex is greedy and will match the LAST valid pattern + const match = 'ABCD1ABC '.match(SEARCH_REGEX); + expect(match).toBeTruthy(); + // It matches BCD as prefix (3 chars), 1 as digit, ABC as suffix + expect(match[1]).toBe('BCD1ABC'); + }); + + test('should NOT match patterns with lowercase letters', () => { + expect('w1aw '.match(SEARCH_REGEX)).toBeNull(); + expect('K2abc '.match(SEARCH_REGEX)).toBeNull(); + }); + + test('should NOT match patterns with special characters', () => { + expect('W-1AW '.match(SEARCH_REGEX)).toBeNull(); + expect('K@2ABC '.match(SEARCH_REGEX)).toBeNull(); + expect('W1A-W '.match(SEARCH_REGEX)).toBeNull(); + }); + + test('should NOT match portable indicators with multiple digits', () => { + expect('W1ABC/34 '.match(SEARCH_REGEX)).toBeNull(); + }); + + test('should NOT match portable indicators with letters', () => { + expect('W1ABC/M '.match(SEARCH_REGEX)).toBeNull(); + expect('W1ABC/P '.match(SEARCH_REGEX)).toBeNull(); + }); + }); + + describe('Edge cases and boundary conditions', () => { + test('should match minimum length call sign (1+1+1)', () => { + expect('W1A '.match(SEARCH_REGEX)).toBeTruthy(); + expect('W1A '.match(SEARCH_REGEX)[1]).toBe('W1A'); + }); + + test('should match maximum length call sign without portable (3+1+3)', () => { + expect('ABC1XYZ '.match(SEARCH_REGEX)).toBeTruthy(); + expect('ABC1XYZ '.match(SEARCH_REGEX)[1]).toBe('ABC1XYZ'); + }); + + test('should match maximum length with portable indicator (3+1+3+/+1)', () => { + expect('ABC1XYZ/5 '.match(SEARCH_REGEX)).toBeTruthy(); + expect('ABC1XYZ/5 '.match(SEARCH_REGEX)[1]).toBe('ABC1XYZ/5'); + }); + + test('should match call signs in the middle of text', () => { + const text = 'I heard W1AW on the air today'; + const match = text.match(SEARCH_REGEX); + expect(match).toBeTruthy(); + expect(match[1]).toBe('W1AW'); + }); + + test('should match first occurrence only per match call', () => { + const text = 'W1AW K2ABC '; + const match = text.match(SEARCH_REGEX); + expect(match[1]).toBe('W1AW'); // Should get first one + }); + + test('should match whitespace characters (\\s includes tab, newline, space)', () => { + // The \\s in regex matches any whitespace, not just space + expect('W1AW\t'.match(SEARCH_REGEX)).toBeTruthy(); + expect('W1AW\n'.match(SEARCH_REGEX)).toBeTruthy(); + expect('W1AW '.match(SEARCH_REGEX)).toBeTruthy(); + }); + + test('should match with comma in prefix (as per regex pattern)', () => { + // The regex allows comma in prefix: [A-Z,\d] + expect('W,1ABC '.match(SEARCH_REGEX)).toBeTruthy(); + expect('A,1X '.match(SEARCH_REGEX)).toBeTruthy(); + }); + }); + + describe('Real-world call sign examples', () => { + test('should match common US call signs', () => { + expect('W1AW '.match(SEARCH_REGEX)[1]).toBe('W1AW'); + expect('K2ABC '.match(SEARCH_REGEX)[1]).toBe('K2ABC'); + expect('N3XYZ '.match(SEARCH_REGEX)[1]).toBe('N3XYZ'); + expect('AA1AA '.match(SEARCH_REGEX)[1]).toBe('AA1AA'); + expect('KD8ABC '.match(SEARCH_REGEX)[1]).toBe('KD8ABC'); + }); + + test('should match common international call signs', () => { + expect('SM8AYA '.match(SEARCH_REGEX)[1]).toBe('SM8AYA'); + expect('DL1ABC '.match(SEARCH_REGEX)[1]).toBe('DL1ABC'); + expect('G0ABC '.match(SEARCH_REGEX)[1]).toBe('G0ABC'); + expect('JA1XYZ '.match(SEARCH_REGEX)[1]).toBe('JA1XYZ'); + expect('VK2DEF '.match(SEARCH_REGEX)[1]).toBe('VK2DEF'); + }); + + test('should match special territory prefixes', () => { + expect('9V1ABC '.match(SEARCH_REGEX)[1]).toBe('9V1ABC'); + expect('3D2XYZ '.match(SEARCH_REGEX)[1]).toBe('3D2XYZ'); + expect('5N1ABC '.match(SEARCH_REGEX)[1]).toBe('5N1ABC'); + }); + }); +}); \ No newline at end of file