@zerospacegg/vynthra
Version:
Discord bot for ZeroSpace.gg data
374 lines (310 loc) • 13.4 kB
text/typescript
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);
});
});
});