UNPKG

@zerospacegg/vynthra

Version:
374 lines (310 loc) 13.4 kB
import { test } from "node:test"; import assert from "node:assert"; import { createFuzzyMatcher, fuzzySearch, contains, startsWith, exactMatch, } from "../../src/lib/fuzzy.js"; // Mock data for testing const mockItems = [ { id: 1, name: "Marine", type: "unit" }, { id: 2, name: "Tank", type: "unit" }, { id: 3, name: "Barracks", type: "building" }, { id: 4, name: "Command Center", type: "building" }, { id: 5, name: "Space Marine", type: "unit" }, { id: 6, name: "Heavy Tank", type: "unit" }, { id: 7, name: "Artillery", type: "unit" }, { id: 8, name: "Factory", type: "building" }, ]; test("Fuzzy Matching Utilities", async (t) => { await t.test("createFuzzyMatcher", async (t) => { await t.test( "should create a fuzzy matcher with items and selector", () => { const matcher = createFuzzyMatcher(mockItems, (item) => item.name); assert.ok(matcher); assert.ok(Array.isArray(matcher.items)); assert.strictEqual(typeof matcher.selector, "function"); assert.strictEqual(matcher.items.length, mockItems.length); }, ); await t.test("should work with different selector functions", () => { const nameSelector = (item: any) => item.name; const typeSelector = (item: any) => item.type; const nameMatcher = createFuzzyMatcher(mockItems, nameSelector); const typeMatcher = createFuzzyMatcher(mockItems, typeSelector); assert.ok(nameMatcher); assert.ok(typeMatcher); assert.strictEqual(nameMatcher.selector, nameSelector); assert.strictEqual(typeMatcher.selector, typeSelector); }); await t.test("should handle empty items array", () => { const matcher = createFuzzyMatcher([], (item: any) => item.name); assert.ok(matcher); assert.strictEqual(matcher.items.length, 0); }); await t.test("should preserve original items", () => { const matcher = createFuzzyMatcher(mockItems, (item) => item.name); assert.deepStrictEqual(matcher.items, mockItems); }); }); await t.test("fuzzySearch", async (t) => { const matcher = createFuzzyMatcher(mockItems, (item) => item.name); await t.test("should return fuzzy search results for valid query", () => { const results = fuzzySearch(matcher, "marine", 5); assert.ok(Array.isArray(results)); assert.ok(results.length > 0); // Check result structure results.forEach((result) => { assert.ok("item" in result); assert.ok("score" in result); assert.ok("positions" in result); assert.strictEqual(typeof result.score, "number"); assert.ok(result.positions instanceof Set); assert.ok(result.score >= 0); }); }); await t.test("should find exact matches", () => { const results = fuzzySearch(matcher, "Marine", 10); assert.ok(results.length > 0); const exactMatch = results.find((r) => r.item.name === "Marine"); assert.ok(exactMatch, "Should find exact match for 'Marine'"); }); await t.test("should find partial matches", () => { const results = fuzzySearch(matcher, "Tank", 10); assert.ok(results.length > 0); // Should find both "Tank" and "Heavy Tank" const tankItems = results.map((r) => r.item.name); assert.ok(tankItems.includes("Tank"), "Should find 'Tank'"); assert.ok(tankItems.includes("Heavy Tank"), "Should find 'Heavy Tank'"); }); await t.test("should respect the limit parameter", () => { const results = fuzzySearch(matcher, "a", 2); assert.ok(results.length <= 2); }); await t.test( "should return results sorted by score (highest first)", () => { const results = fuzzySearch(matcher, "tank", 10); if (results.length > 1) { for (let i = 1; i < results.length; i++) { assert.ok( results[i - 1].score >= results[i].score, `Result ${i - 1} (score: ${results[i - 1].score}) should have higher or equal score than result ${i} (score: ${results[i].score})`, ); } } }, ); await t.test("should handle empty query", () => { const results = fuzzySearch(matcher, "", 5); assert.ok(Array.isArray(results)); // Empty query typically returns no results with fuzzy library }); await t.test("should handle query with no matches", () => { const results = fuzzySearch(matcher, "xyz123notfound", 5); assert.ok(Array.isArray(results)); // May return empty array or low-scoring results }); await t.test("should handle case-insensitive matching", () => { const lowerResults = fuzzySearch(matcher, "marine", 5); const upperResults = fuzzySearch(matcher, "MARINE", 5); const mixedResults = fuzzySearch(matcher, "Marine", 5); // All should find some results (fuzzy library handles case) assert.ok(Array.isArray(lowerResults)); assert.ok(Array.isArray(upperResults)); assert.ok(Array.isArray(mixedResults)); }); await t.test("should work with substring queries", () => { const results = fuzzySearch(matcher, "mar", 5); assert.ok(Array.isArray(results)); // Should find items containing "mar" like "Marine" and "Space Marine" if (results.length > 0) { const hasMarineMatch = results.some((r) => r.item.name.toLowerCase().includes("mar"), ); assert.ok(hasMarineMatch, "Should find items containing 'mar'"); } }); await t.test("should handle special characters in query gracefully", () => { const results = fuzzySearch(matcher, "tank-", 5); assert.ok(Array.isArray(results)); // Should not throw errors }); await t.test("should work with different item types", () => { const unitMatcher = createFuzzyMatcher(mockItems, (item) => item.type); const results = fuzzySearch(unitMatcher, "unit", 10); assert.ok(Array.isArray(results)); // Should find items where type matches "unit" }); }); await t.test("contains", async (t) => { await t.test("should return true for exact substring matches", () => { assert.ok(contains("test", "testing")); assert.ok(contains("ing", "testing")); assert.ok(contains("est", "testing")); }); await t.test("should return false for non-matching strings", () => { assert.ok(!contains("xyz", "testing")); assert.ok(!contains("abc", "testing")); }); await t.test("should be case insensitive", () => { assert.ok(contains("TEST", "testing")); assert.ok(contains("Test", "TESTING")); assert.ok(contains("tEsT", "TeStInG")); }); await t.test("should handle empty strings", () => { assert.ok(contains("", "testing")); // Empty string is contained in any string assert.ok(!contains("test", "")); // Non-empty string not contained in empty string assert.ok(contains("", "")); // Empty string contains empty string }); await t.test("should handle null/undefined inputs gracefully", () => { assert.ok(!contains(null as any, "testing")); assert.ok(!contains("test", null as any)); assert.ok(!contains(undefined as any, "testing")); assert.ok(!contains("test", undefined as any)); assert.ok(!contains(null as any, null as any)); }); await t.test("should handle same strings", () => { assert.ok(contains("testing", "testing")); assert.ok(contains("TESTING", "testing")); }); await t.test("should handle special characters", () => { assert.ok(contains("test-", "test-case")); assert.ok(contains("test_", "test_case")); assert.ok(contains("test.", "test.case")); }); await t.test("should handle unicode characters", () => { assert.ok(contains("ä", "tëst-cäse")); assert.ok(contains("ë", "tëst-case")); }); }); await t.test("startsWith", async (t) => { await t.test("should return true for strings that start with query", () => { assert.ok(startsWith("test", "testing")); assert.ok(startsWith("hello", "hello world")); }); await t.test( "should return false for strings that do not start with query", () => { assert.ok(!startsWith("ing", "testing")); assert.ok(!startsWith("world", "hello world")); }, ); await t.test("should be case insensitive", () => { assert.ok(startsWith("TEST", "testing")); assert.ok(startsWith("Test", "TESTING")); assert.ok(startsWith("tEsT", "TeStInG")); }); await t.test("should handle empty strings", () => { assert.ok(startsWith("", "testing")); // Empty string starts any string assert.ok(!startsWith("test", "")); // Non-empty string doesn't start empty string assert.ok(startsWith("", "")); // Empty string starts empty string }); await t.test("should handle null/undefined inputs gracefully", () => { assert.ok(!startsWith(null as any, "testing")); assert.ok(!startsWith("test", null as any)); assert.ok(!startsWith(undefined as any, "testing")); assert.ok(!startsWith("test", undefined as any)); assert.ok(!startsWith(null as any, null as any)); }); await t.test("should handle same strings", () => { assert.ok(startsWith("testing", "testing")); assert.ok(startsWith("TESTING", "testing")); }); await t.test("should handle longer query than target", () => { assert.ok(!startsWith("testing123", "test")); }); await t.test("should handle special characters", () => { assert.ok(startsWith("test-", "test-case")); assert.ok(startsWith("test_", "test_case")); assert.ok(startsWith("test.", "test.case")); }); await t.test("should handle unicode characters", () => { assert.ok(startsWith("tëst", "tëst-case")); assert.ok(!startsWith("tëst", "test-case")); }); await t.test("should handle numbers", () => { assert.ok(startsWith("123", "123test")); assert.ok(!startsWith("456", "123test")); }); }); await t.test("exactMatch", async (t) => { await t.test("should return true for exact matches", () => { assert.ok(exactMatch("test", "test")); assert.ok(exactMatch("hello", "hello")); }); await t.test("should return false for non-matching strings", () => { assert.ok(!exactMatch("test", "testing")); assert.ok(!exactMatch("hello", "world")); }); await t.test("should be case insensitive", () => { assert.ok(exactMatch("TEST", "test")); assert.ok(exactMatch("Test", "TEST")); assert.ok(exactMatch("tEsT", "TeSt")); }); await t.test("should handle empty strings", () => { assert.ok(exactMatch("", "")); assert.ok(!exactMatch("test", "")); assert.ok(!exactMatch("", "test")); }); await t.test("should handle null/undefined inputs gracefully", () => { assert.ok(!exactMatch(null as any, "test")); assert.ok(!exactMatch("test", null as any)); assert.ok(!exactMatch(undefined as any, "test")); assert.ok(!exactMatch("test", undefined as any)); assert.ok(!exactMatch(null as any, null as any)); }); await t.test("should handle special characters", () => { assert.ok(exactMatch("test-case", "test-case")); assert.ok(exactMatch("test_case", "test_case")); assert.ok(exactMatch("test.case", "test.case")); }); await t.test("should handle unicode characters", () => { assert.ok(exactMatch("tëst-case", "tëst-case")); assert.ok(!exactMatch("tëst-case", "test-case")); }); await t.test("should handle numbers", () => { assert.ok(exactMatch("123", "123")); assert.ok(!exactMatch("123", "456")); }); await t.test("should handle whitespace", () => { assert.ok(exactMatch("hello world", "hello world")); assert.ok(!exactMatch("hello world", "hello world")); // Different whitespace assert.ok(exactMatch(" test ", " test ")); // Leading/trailing spaces }); }); await t.test("Integration tests", async (t) => { await t.test("should work together for complete search workflow", () => { const items = [ { name: "Terran Marine", faction: "Terran" }, { name: "Protoss Zealot", faction: "Protoss" }, { name: "Zerg Zergling", faction: "Zerg" }, { name: "Marine Artillery", faction: "Terran" }, ]; const nameMatcher = createFuzzyMatcher(items, (item) => item.name); // Test fuzzy search finds relevant items const marineResults = fuzzySearch(nameMatcher, "marine", 10); assert.ok(marineResults.length > 0); // Verify we can find specific items const hasMarineMatch = marineResults.some((r) => exactMatch("Terran Marine", r.item.name), ); const hasArtilleryMatch = marineResults.some((r) => contains("Artillery", r.item.name), ); assert.ok( hasMarineMatch || hasArtilleryMatch, "Should find items related to 'marine'", ); }); await t.test("should handle edge cases gracefully", () => { const emptyMatcher = createFuzzyMatcher([], (item: any) => item.name); const emptyResults = fuzzySearch(emptyMatcher, "test", 5); assert.ok(Array.isArray(emptyResults)); assert.strictEqual(emptyResults.length, 0); }); }); });