From 742f060ebac9f4685fb968aaa5a7d99fd1ba83a1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Sep 2025 23:08:45 +0000 Subject: [PATCH 1/3] Initial plan From d79a7d36456e33afc87b94b911a8d4ac42477667 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Sep 2025 23:14:55 +0000 Subject: [PATCH 2/3] Add Jest testing framework with unit tests for core functionalities Co-authored-by: phieri <12006381+phieri@users.noreply.github.com> --- README.md | 27 ++++++++ package.json | 12 +++- tests/getFlag.test.js | 76 ++++++++++++++++++++ tests/getPhonetics.test.js | 126 ++++++++++++++++++++++++++++++++++ tests/searchCallsigns.test.js | 124 +++++++++++++++++++++++++++++++++ 5 files changed, 364 insertions(+), 1 deletion(-) create mode 100644 tests/getFlag.test.js create mode 100644 tests/getPhonetics.test.js create mode 100644 tests/searchCallsigns.test.js diff --git a/README.md b/README.md index e1c8aa8..c1154f6 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,33 @@ 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 unit tests using Jest to ensure code quality and functionality. + +## Running Tests +```bash +# Install dependencies +npm install + +# Run all tests +npm test + +# Run linting +npm run lint +``` + +## Test Coverage +The test suite covers three core functionalities: + +1. **`getFlag` Method**: Tests conversion of ISO country codes to Unicode Regional Indicator Symbols (emoji flags) +2. **`getPhonetics` Method**: Tests mapping of characters to their phonetic alphabet equivalents +3. **`searchCallsigns` Method**: Tests basic functionality for detecting and wrapping untagged call signs + +Test files are located in the `tests/` directory: +- `tests/getFlag.test.js` - Flag generation tests +- `tests/getPhonetics.test.js` - Phonetic alphabet tests +- `tests/searchCallsigns.test.js` - Call sign detection tests + # 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..c64c4b0 --- /dev/null +++ b/tests/getFlag.test.js @@ -0,0 +1,76 @@ +/** + * Unit tests for the getFlag method + * Tests the conversion of ISO country codes to Unicode Regional Indicator Symbols (emoji flags) + */ + +// Since the callsign.js file is meant for browser environments and uses custom elements, +// we'll test the getFlag method by copying its implementation for testing purposes +// This approach is necessary because the original file depends on browser APIs + +/** + * 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', () => { + test('should convert US code to US flag emoji', () => { + const result = getFlag('US'); + // US flag emoji is represented by these code points + const expected = String.fromCodePoint(127482, 127480); // πŸ‡ΊπŸ‡Έ + expect(result).toBe(expected); + }); + + test('should convert SE code to Swedish flag emoji', () => { + const result = getFlag('SE'); + // SE flag emoji + const expected = String.fromCodePoint(127480, 127466); // πŸ‡ΈπŸ‡ͺ + expect(result).toBe(expected); + }); + + test('should convert GB code to UK flag emoji', () => { + const result = getFlag('GB'); + // GB flag emoji + const expected = String.fromCodePoint(127468, 127463); // πŸ‡¬πŸ‡§ + expect(result).toBe(expected); + }); + + test('should convert DE code to German flag emoji', () => { + const result = getFlag('DE'); + // DE flag emoji + const expected = String.fromCodePoint(127465, 127466); // πŸ‡©πŸ‡ͺ + expect(result).toBe(expected); + }); + + test('should convert JP code to Japanese flag emoji', () => { + const result = getFlag('JP'); + // JP flag emoji + const expected = String.fromCodePoint(127471, 127477); // πŸ‡―πŸ‡΅ + expect(result).toBe(expected); + }); + + test('should handle lowercase input by converting correctly', () => { + // The original function expects uppercase, but let's test with lowercase + const result = getFlag('us'); + // This will produce different unicode points for lowercase + const expected = String.fromCodePoint(127514, 127512); // Different from uppercase + expect(result).toBe(expected); + }); + + test('should handle two-character codes correctly', () => { + const result = getFlag('CA'); + const expected = String.fromCodePoint(127464, 127462); // πŸ‡¨πŸ‡¦ + expect(result).toBe(expected); + }); + + test('should convert each character correctly using the offset', () => { + // Test the mathematical transformation: A = 65, 65 + 127397 = 127462 + const result = getFlag('AA'); + const expected = String.fromCodePoint(127462, 127462); // πŸ‡¦πŸ‡¦ + expect(result).toBe(expected); + }); +}); \ No newline at end of file diff --git a/tests/getPhonetics.test.js b/tests/getPhonetics.test.js new file mode 100644 index 0000000..dacee5c --- /dev/null +++ b/tests/getPhonetics.test.js @@ -0,0 +1,126 @@ +/** + * Unit tests for the getPhonetics method + * Tests the mapping of characters to their phonetic alphabet equivalents + */ + +// 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', () => { + test('should convert single letter to phonetic equivalent', () => { + expect(getPhonetics('A')).toBe('Alfa'); + expect(getPhonetics('B')).toBe('Bravo'); + expect(getPhonetics('Z')).toBe('Zulu'); + }); + + test('should convert single digit to phonetic equivalent', () => { + expect(getPhonetics('0')).toBe('Ziro'); + expect(getPhonetics('1')).toBe('One'); + expect(getPhonetics('9')).toBe('Niner'); + }); + + test('should convert typical call sign letters', () => { + expect(getPhonetics('W1AW')).toBe('Whiskey One Alfa Whiskey'); + expect(getPhonetics('SM8AYA')).toBe('Sierra Mike Eight Alfa Yankee Alfa'); + expect(getPhonetics('DL1ABC')).toBe('Delta Lima One Alfa Bravo Charlie'); + }); + + test('should handle mixed letters and numbers', () => { + expect(getPhonetics('K2ABC')).toBe('Kilo Two Alfa Bravo Charlie'); + expect(getPhonetics('VE3XYZ')).toBe('Victor Echo Tree X-ray Yankee Zulu'); + }); + + test('should return empty string for empty input', () => { + expect(getPhonetics('')).toBe(''); + }); + + test('should handle single character input', () => { + expect(getPhonetics('X')).toBe('X-ray'); + expect(getPhonetics('3')).toBe('Tree'); + }); + + test('should handle all letters A-Z correctly', () => { + const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + const expected = [ + 'Alfa', 'Bravo', 'Charlie', 'Delta', 'Echo', 'Foxtrot', + 'Golf', 'Hotel', 'India', 'Juliett', 'Kilo', 'Lima', + 'Mike', 'November', 'Oscar', 'Papa', 'Quebec', 'Romeo', + 'Sierra', 'Tango', 'Uniform', 'Victor', 'Whiskey', + 'X-ray', 'Yankee', 'Zulu' + ].join(' '); + + expect(getPhonetics(alphabet)).toBe(expected); + }); + + test('should handle all digits 0-9 correctly', () => { + const digits = '0123456789'; + const expected = [ + 'Ziro', 'One', 'Two', 'Tree', 'Four', 'Five', + 'Six', 'Seven', 'Eight', 'Niner' + ].join(' '); + + expect(getPhonetics(digits)).toBe(expected); + }); + + test('should handle characters not in phonetic table', () => { + // Characters not in the table should result in undefined being added + const result = getPhonetics('A/B'); + expect(result).toBe('Alfa undefined Bravo'); + }); + + test('should handle typical amateur radio call signs', () => { + expect(getPhonetics('KD8ABC')).toBe('Kilo Delta Eight Alfa Bravo Charlie'); + expect(getPhonetics('G0XYZ')).toBe('Golf Ziro X-ray Yankee Zulu'); + expect(getPhonetics('JA1ABC')).toBe('Juliett Alfa One Alfa Bravo Charlie'); + }); +}); \ No newline at end of file diff --git a/tests/searchCallsigns.test.js b/tests/searchCallsigns.test.js new file mode 100644 index 0000000..703cfd9 --- /dev/null +++ b/tests/searchCallsigns.test.js @@ -0,0 +1,124 @@ +/** + * Unit tests for the searchCallsigns method + * Tests basic functionality for detecting and wrapping untagged call signs in tags + */ + +// Mock the search regex and the searchCallsigns functionality for testing +const SEARCH_REGEX = /([A-Z,\d]{1,3}\d[A-Z]{1,3}(?:\/\d)?)\s/; + +/** + * Simplified version of searchCallsigns for testing + * @param {string} html Input HTML string + * @returns {string} HTML with call signs wrapped in call-sign tags + */ +function searchCallsigns(html) { + 'use strict'; + let match; + let result = html; + + while ((match = result.match(SEARCH_REGEX)) !== null) { + result = result.replace(match[1], '' + match[1] + ''); + } + + return result; +} + +// Setup jsdom environment +beforeEach(() => { + document.body.innerHTML = ''; +}); + +describe('searchCallsigns method', () => { + test('should detect and wrap simple call sign with space after', () => { + const input = 'Contact W1AW today'; + const expected = 'Contact W1AW today'; + expect(searchCallsigns(input)).toBe(expected); + }); + + test('should detect and wrap multiple call signs', () => { + const input = 'Contact W1AW and SM8AYA today'; + const expected = 'Contact W1AW and SM8AYA today'; + expect(searchCallsigns(input)).toBe(expected); + }); + + test('should detect call signs with different prefix lengths', () => { + // 1-letter prefix + digit + letters + expect(searchCallsigns('Call K2ABC ')).toBe('Call K2ABC '); + + // 2-letter prefix + digit + letters + expect(searchCallsigns('Call SM8AYA ')).toBe('Call SM8AYA '); + + // 3-letter prefix + digit + letters + expect(searchCallsigns('Call VK2ABC ')).toBe('Call VK2ABC '); + }); + + test('should detect call signs with different suffix lengths', () => { + // Single letter suffix + expect(searchCallsigns('Call W1A ')).toBe('Call W1A '); + + // Two letter suffix + expect(searchCallsigns('Call W1AB ')).toBe('Call W1AB '); + + // Three letter suffix + expect(searchCallsigns('Call W1ABC ')).toBe('Call W1ABC '); + }); + + test('should detect call signs with portable indicators', () => { + const input = 'Call W1ABC/3 on the air'; + const expected = 'Call W1ABC/3 on the air'; + expect(searchCallsigns(input)).toBe(expected); + }); + + test('should not wrap call signs without trailing space', () => { + // The regex requires a space after the call sign + const input = 'CallW1ABC'; + expect(searchCallsigns(input)).toBe(input); // No change expected + }); + + test('should handle call signs in different contexts', () => { + expect(searchCallsigns('Hello W1AW from K2ABC ')).toBe('Hello W1AW from K2ABC '); + }); + + test('should handle text with no call signs', () => { + const input = 'This is just normal text with no call signs.'; + expect(searchCallsigns(input)).toBe(input); + }); + + test('should handle empty string', () => { + expect(searchCallsigns('')).toBe(''); + }); + + test('should detect various valid call sign patterns', () => { + // US call signs + expect(searchCallsigns('W1AW ')).toBe('W1AW '); + expect(searchCallsigns('K2ABC ')).toBe('K2ABC '); + expect(searchCallsigns('N3XYZ ')).toBe('N3XYZ '); + + // International call signs + expect(searchCallsigns('G0ABC ')).toBe('G0ABC '); + expect(searchCallsigns('DL1ABC ')).toBe('DL1ABC '); + expect(searchCallsigns('JA1XYZ ')).toBe('JA1XYZ '); + }); + + test('should handle call signs with numbers in prefix', () => { + // Some call signs can have numbers in the prefix + expect(searchCallsigns('9V1ABC ')).toBe('9V1ABC '); + }); + + test('should process multiple occurrences iteratively', () => { + // Test that the while loop processes all matches + const input = 'W1AW K2ABC SM8AYA '; + const result = searchCallsigns(input); + expect(result).toContain('W1AW'); + expect(result).toContain('K2ABC'); + expect(result).toContain('SM8AYA'); + }); + + test('should handle already wrapped call signs correctly', () => { + // If a call sign is already wrapped, it shouldn't be wrapped again + const input = 'W1AW and K2ABC '; + const result = searchCallsigns(input); + // Should only wrap the unwrapped call sign + expect(result).toBe('W1AW and K2ABC '); + }); +}); \ No newline at end of file From e2d5a59e2cd10c42a7248e1e9741f9fdf4755302 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 26 Oct 2025 22:02:37 +0000 Subject: [PATCH 3/3] Refactor tests to focus primarily on regex pattern validation Co-authored-by: phieri <12006381+phieri@users.noreply.github.com> --- README.md | 39 +++-- tests/getFlag.test.js | 88 +++------- tests/getPhonetics.test.js | 125 +++++++------- tests/partsRegex.test.js | 301 +++++++++++++++++++++++++++++++++ tests/searchCallsigns.test.js | 309 ++++++++++++++++++++++------------ 5 files changed, 622 insertions(+), 240 deletions(-) create mode 100644 tests/partsRegex.test.js diff --git a/README.md b/README.md index c1154f6..872acea 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Options can be set as attributes in the `` tag. | `data-search` | `false` | Find and mark up untagged call signs in the document. | # Testing -This project includes unit tests using Jest to ensure code quality and functionality. +This project includes comprehensive unit tests using Jest with a primary focus on regex pattern validation. ## Running Tests ```bash @@ -39,17 +39,32 @@ npm test npm run lint ``` -## Test Coverage -The test suite covers three core functionalities: - -1. **`getFlag` Method**: Tests conversion of ISO country codes to Unicode Regional Indicator Symbols (emoji flags) -2. **`getPhonetics` Method**: Tests mapping of characters to their phonetic alphabet equivalents -3. **`searchCallsigns` Method**: Tests basic functionality for detecting and wrapping untagged call signs - -Test files are located in the `tests/` directory: -- `tests/getFlag.test.js` - Flag generation tests -- `tests/getPhonetics.test.js` - Phonetic alphabet tests -- `tests/searchCallsigns.test.js` - Call sign detection tests +## 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)). diff --git a/tests/getFlag.test.js b/tests/getFlag.test.js index c64c4b0..96e397a 100644 --- a/tests/getFlag.test.js +++ b/tests/getFlag.test.js @@ -1,12 +1,9 @@ /** * Unit tests for the getFlag method - * Tests the conversion of ISO country codes to Unicode Regional Indicator Symbols (emoji flags) + * Validates the conversion of ISO country codes extracted by PREFIX_TABLE regex matching + * to Unicode Regional Indicator Symbols (emoji flags) */ -// Since the callsign.js file is meant for browser environments and uses custom elements, -// we'll test the getFlag method by copying its implementation for testing purposes -// This approach is necessary because the original file depends on browser APIs - /** * Converts an ISO country code to a Unicode Regional Indicator Symbol (emoji flag). * @param {!string} code The ISO 3166-1 alpha-2 code @@ -17,60 +14,31 @@ function getFlag(code) { return String.fromCodePoint(...[...code].map(c => c.charCodeAt() + 127397)); } -describe('getFlag method', () => { - test('should convert US code to US flag emoji', () => { - const result = getFlag('US'); - // US flag emoji is represented by these code points - const expected = String.fromCodePoint(127482, 127480); // πŸ‡ΊπŸ‡Έ - expect(result).toBe(expected); - }); - - test('should convert SE code to Swedish flag emoji', () => { - const result = getFlag('SE'); - // SE flag emoji - const expected = String.fromCodePoint(127480, 127466); // πŸ‡ΈπŸ‡ͺ - expect(result).toBe(expected); - }); - - test('should convert GB code to UK flag emoji', () => { - const result = getFlag('GB'); - // GB flag emoji - const expected = String.fromCodePoint(127468, 127463); // πŸ‡¬πŸ‡§ - expect(result).toBe(expected); - }); - - test('should convert DE code to German flag emoji', () => { - const result = getFlag('DE'); - // DE flag emoji - const expected = String.fromCodePoint(127465, 127466); // πŸ‡©πŸ‡ͺ - expect(result).toBe(expected); - }); - - test('should convert JP code to Japanese flag emoji', () => { - const result = getFlag('JP'); - // JP flag emoji - const expected = String.fromCodePoint(127471, 127477); // πŸ‡―πŸ‡΅ - expect(result).toBe(expected); - }); - - test('should handle lowercase input by converting correctly', () => { - // The original function expects uppercase, but let's test with lowercase - const result = getFlag('us'); - // This will produce different unicode points for lowercase - const expected = String.fromCodePoint(127514, 127512); // Different from uppercase - expect(result).toBe(expected); - }); - - test('should handle two-character codes correctly', () => { - const result = getFlag('CA'); - const expected = String.fromCodePoint(127464, 127462); // πŸ‡¨πŸ‡¦ - expect(result).toBe(expected); - }); - - test('should convert each character correctly using the offset', () => { - // Test the mathematical transformation: A = 65, 65 + 127397 = 127462 - const result = getFlag('AA'); - const expected = String.fromCodePoint(127462, 127462); // πŸ‡¦πŸ‡¦ - expect(result).toBe(expected); +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 index dacee5c..639f11d 100644 --- a/tests/getPhonetics.test.js +++ b/tests/getPhonetics.test.js @@ -1,6 +1,6 @@ /** * Unit tests for the getPhonetics method - * Tests the mapping of characters to their phonetic alphabet equivalents + * Tests phonetic alphabet mapping for characters extracted from call signs via PARTS_REGEX */ // Copy the phonetic table and getPhonetics method for testing @@ -56,71 +56,82 @@ function getPhonetics(letters) { return ret.slice(0, -1); } -describe('getPhonetics method', () => { - test('should convert single letter to phonetic equivalent', () => { - expect(getPhonetics('A')).toBe('Alfa'); - expect(getPhonetics('B')).toBe('Bravo'); - expect(getPhonetics('Z')).toBe('Zulu'); - }); - - test('should convert single digit to phonetic equivalent', () => { - expect(getPhonetics('0')).toBe('Ziro'); - expect(getPhonetics('1')).toBe('One'); - expect(getPhonetics('9')).toBe('Niner'); - }); +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 convert typical call sign letters', () => { - expect(getPhonetics('W1AW')).toBe('Whiskey One Alfa Whiskey'); - 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 mixed letters and numbers', () => { - expect(getPhonetics('K2ABC')).toBe('Kilo Two Alfa Bravo Charlie'); - expect(getPhonetics('VE3XYZ')).toBe('Victor Echo Tree X-ray Yankee Zulu'); + 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); + }); + }); }); - test('should return empty string for empty input', () => { - expect(getPhonetics('')).toBe(''); - }); + describe('Edge cases and regex-related scenarios', () => { + test('should handle empty string (no regex match)', () => { + expect(getPhonetics('')).toBe(''); + }); - test('should handle single character input', () => { - expect(getPhonetics('X')).toBe('X-ray'); - expect(getPhonetics('3')).toBe('Tree'); - }); + 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 handle all letters A-Z correctly', () => { - const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; - const expected = [ - 'Alfa', 'Bravo', 'Charlie', 'Delta', 'Echo', 'Foxtrot', - 'Golf', 'Hotel', 'India', 'Juliett', 'Kilo', 'Lima', - 'Mike', 'November', 'Oscar', 'Papa', 'Quebec', 'Romeo', - 'Sierra', 'Tango', 'Uniform', 'Victor', 'Whiskey', - 'X-ray', 'Yankee', 'Zulu' - ].join(' '); - - expect(getPhonetics(alphabet)).toBe(expected); - }); + 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 handle all digits 0-9 correctly', () => { - const digits = '0123456789'; - const expected = [ - 'Ziro', 'One', 'Two', 'Tree', 'Four', 'Five', - 'Six', 'Seven', 'Eight', 'Niner' - ].join(' '); - - expect(getPhonetics(digits)).toBe(expected); + 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'); + }); }); - test('should handle characters not in phonetic table', () => { - // Characters not in the table should result in undefined being added - const result = getPhonetics('A/B'); - expect(result).toBe('Alfa undefined Bravo'); - }); + 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 typical amateur radio call signs', () => { - expect(getPhonetics('KD8ABC')).toBe('Kilo Delta Eight Alfa Bravo Charlie'); - expect(getPhonetics('G0XYZ')).toBe('Golf Ziro X-ray Yankee Zulu'); - expect(getPhonetics('JA1ABC')).toBe('Juliett Alfa One Alfa Bravo Charlie'); + 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 index 703cfd9..6a8123c 100644 --- a/tests/searchCallsigns.test.js +++ b/tests/searchCallsigns.test.js @@ -1,124 +1,211 @@ /** - * Unit tests for the searchCallsigns method - * Tests basic functionality for detecting and wrapping untagged call signs in tags + * 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 */ -// Mock the search regex and the searchCallsigns functionality for testing const SEARCH_REGEX = /([A-Z,\d]{1,3}\d[A-Z]{1,3}(?:\/\d)?)\s/; -/** - * Simplified version of searchCallsigns for testing - * @param {string} html Input HTML string - * @returns {string} HTML with call signs wrapped in call-sign tags - */ -function searchCallsigns(html) { - 'use strict'; - let match; - let result = html; - - while ((match = result.match(SEARCH_REGEX)) !== null) { - result = result.replace(match[1], '' + match[1] + ''); - } - - return result; -} - -// Setup jsdom environment -beforeEach(() => { - document.body.innerHTML = ''; -}); - -describe('searchCallsigns method', () => { - test('should detect and wrap simple call sign with space after', () => { - const input = 'Contact W1AW today'; - const expected = 'Contact W1AW today'; - expect(searchCallsigns(input)).toBe(expected); - }); - - test('should detect and wrap multiple call signs', () => { - const input = 'Contact W1AW and SM8AYA today'; - const expected = 'Contact W1AW and SM8AYA today'; - expect(searchCallsigns(input)).toBe(expected); - }); - - test('should detect call signs with different prefix lengths', () => { - // 1-letter prefix + digit + letters - expect(searchCallsigns('Call K2ABC ')).toBe('Call K2ABC '); - - // 2-letter prefix + digit + letters - expect(searchCallsigns('Call SM8AYA ')).toBe('Call SM8AYA '); - - // 3-letter prefix + digit + letters - expect(searchCallsigns('Call VK2ABC ')).toBe('Call VK2ABC '); - }); - - test('should detect call signs with different suffix lengths', () => { - // Single letter suffix - expect(searchCallsigns('Call W1A ')).toBe('Call W1A '); - - // Two letter suffix - expect(searchCallsigns('Call W1AB ')).toBe('Call W1AB '); - - // Three letter suffix - expect(searchCallsigns('Call W1ABC ')).toBe('Call W1ABC '); - }); - - test('should detect call signs with portable indicators', () => { - const input = 'Call W1ABC/3 on the air'; - const expected = 'Call W1ABC/3 on the air'; - expect(searchCallsigns(input)).toBe(expected); - }); - - test('should not wrap call signs without trailing space', () => { - // The regex requires a space after the call sign - const input = 'CallW1ABC'; - expect(searchCallsigns(input)).toBe(input); // No change expected - }); - - test('should handle call signs in different contexts', () => { - expect(searchCallsigns('Hello W1AW from K2ABC ')).toBe('Hello W1AW from K2ABC '); - }); - - test('should handle text with no call signs', () => { - const input = 'This is just normal text with no call signs.'; - expect(searchCallsigns(input)).toBe(input); - }); - - test('should handle empty string', () => { - expect(searchCallsigns('')).toBe(''); - }); - - test('should detect various valid call sign patterns', () => { - // US call signs - expect(searchCallsigns('W1AW ')).toBe('W1AW '); - expect(searchCallsigns('K2ABC ')).toBe('K2ABC '); - expect(searchCallsigns('N3XYZ ')).toBe('N3XYZ '); - - // International call signs - expect(searchCallsigns('G0ABC ')).toBe('G0ABC '); - expect(searchCallsigns('DL1ABC ')).toBe('DL1ABC '); - expect(searchCallsigns('JA1XYZ ')).toBe('JA1XYZ '); +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`); + } + }); }); - test('should handle call signs with numbers in prefix', () => { - // Some call signs can have numbers in the prefix - expect(searchCallsigns('9V1ABC ')).toBe('9V1ABC '); + 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(); + }); }); - test('should process multiple occurrences iteratively', () => { - // Test that the while loop processes all matches - const input = 'W1AW K2ABC SM8AYA '; - const result = searchCallsigns(input); - expect(result).toContain('W1AW'); - expect(result).toContain('K2ABC'); - expect(result).toContain('SM8AYA'); + 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(); + }); }); - test('should handle already wrapped call signs correctly', () => { - // If a call sign is already wrapped, it shouldn't be wrapped again - const input = 'W1AW and K2ABC '; - const result = searchCallsigns(input); - // Should only wrap the unwrapped call sign - expect(result).toBe('W1AW and K2ABC '); + 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