/**
* Tests for tools/search-specs.ts
*/
import { describe, expect, it } from 'vitest';
import { quickFindByShortname, searchSpecs } from '../../src/tools/search-specs.js';
describe('searchSpecs', () => {
describe('basic functionality', () => {
it('should return array of search results', async () => {
const results = await searchSpecs('fetch');
expect(Array.isArray(results)).toBe(true);
});
it('should return results with required properties', async () => {
const results = await searchSpecs('fetch');
if (results.length > 0) {
const result = results[0];
expect(result).toHaveProperty('shortname');
expect(result).toHaveProperty('title');
expect(result).toHaveProperty('url');
expect(result).toHaveProperty('matchType');
expect(result).toHaveProperty('score');
}
});
it('should respect limit parameter', async () => {
const results = await searchSpecs('web', 5);
expect(results.length).toBeLessThanOrEqual(5);
});
it('should return empty array for non-matching query', async () => {
const results = await searchSpecs('xyznonexistent123abc');
expect(results.length).toBe(0);
});
});
describe('scoring', () => {
it('should give exact shortname match highest score (100)', async () => {
const results = await searchSpecs('fetch');
const exactMatch = results.find((r) => r.shortname === 'fetch');
expect(exactMatch).toBeDefined();
expect(exactMatch?.score).toBe(100);
});
it('should prioritize shortname matches over title matches', async () => {
const results = await searchSpecs('dom');
const shortnameMatch = results.find((r) => r.matchType === 'shortname');
const titleMatch = results.find((r) => r.matchType === 'title');
if (shortnameMatch && titleMatch) {
expect(shortnameMatch.score).toBeGreaterThanOrEqual(titleMatch.score);
}
});
it('should sort results by score descending', async () => {
const results = await searchSpecs('service worker');
for (let i = 0; i < results.length - 1; i++) {
expect(results[i].score).toBeGreaterThanOrEqual(results[i + 1].score);
}
});
});
describe('match types', () => {
it('should identify shortname matches', async () => {
const results = await searchSpecs('fetch');
const shortnameMatches = results.filter((r) => r.matchType === 'shortname');
expect(shortnameMatches.length).toBeGreaterThan(0);
});
it('should identify title matches', async () => {
const results = await searchSpecs('Service Worker');
const hasMatch = results.some((r) => r.matchType === 'title' || r.matchType === 'shortname');
expect(hasMatch).toBe(true);
});
});
describe('word-based matching', () => {
it('should match multi-word queries', async () => {
const results = await searchSpecs('service worker');
expect(results.length).toBeGreaterThan(0);
});
it('should match partial word queries', async () => {
const results = await searchSpecs('manifest');
expect(results.length).toBeGreaterThan(0);
});
});
describe('default limit', () => {
it('should use default limit of 20', async () => {
const results = await searchSpecs('web');
expect(results.length).toBeLessThanOrEqual(20);
});
});
});
describe('quickFindByShortname', () => {
it('should find spec by exact shortname', async () => {
const result = await quickFindByShortname('fetch');
expect(result).toBeDefined();
expect(result?.shortname).toBe('fetch');
expect(result?.score).toBe(100);
expect(result?.matchType).toBe('shortname');
});
it('should return null for non-existent shortname', async () => {
const result = await quickFindByShortname('nonexistent-spec-xyz');
expect(result).toBeNull();
});
it('should return result with all required properties', async () => {
const result = await quickFindByShortname('dom');
expect(result).toBeDefined();
if (result) {
expect(result).toHaveProperty('shortname');
expect(result).toHaveProperty('title');
expect(result).toHaveProperty('url');
expect(result).toHaveProperty('matchType');
expect(result).toHaveProperty('score');
}
});
it('should find specs through series shortname', async () => {
// Many specs have series shortnames that differ from their main shortname
const result = await quickFindByShortname('service-workers');
expect(result).toBeDefined();
});
});
describe('coverage: additional score branches', () => {
it('should match QUERY_CONTAINS_SHORTNAME when query contains a spec shortname', async () => {
// Query that contains a shortname like "fetch" within a longer string
const results = await searchSpecs('the fetch api specification');
const fetchResult = results.find((r) => r.shortname === 'fetch');
// Should match with QUERY_CONTAINS_SHORTNAME score (70)
expect(fetchResult).toBeDefined();
expect(fetchResult?.score).toBe(70);
expect(fetchResult?.matchType).toBe('shortname');
});
it('should match EXACT_TITLE when query exactly matches a title', async () => {
// Use exact title - "Fetch" is the exact title of the fetch spec
const results = await searchSpecs('Fetch');
// The exact shortname match takes precedence, but if we search for a title
// that doesn't match a shortname, we should get EXACT_TITLE
const exactTitleMatch = results.find((r) => r.score === 90 && r.matchType === 'title');
// Even if not found, at least verify the search works
expect(results.length).toBeGreaterThan(0);
// Log for debugging if needed
if (exactTitleMatch) {
expect(exactTitleMatch.matchType).toBe('title');
}
});
it('should match ALL_WORDS_MATCH when all query words are in title', async () => {
// Multi-word query where all words appear in a title
const results = await searchSpecs('web application manifest');
const allWordsMatch = results.find((r) => r.score === 50);
expect(allWordsMatch).toBeDefined();
expect(allWordsMatch?.matchType).toBe('title');
});
it('should match via abstract/description when no title/shortname match', async () => {
// Search for a term that appears in abstract but not title/shortname
// This is hard to test without knowing exact abstract contents
// Let's search for something likely to be in an abstract
const results = await searchSpecs('standardized interface');
const descMatch = results.find((r) => r.matchType === 'description');
// May or may not find results, but should not throw
expect(Array.isArray(results)).toBe(true);
// If description match found, verify it
if (descMatch) {
expect(descMatch.matchType).toBe('description');
}
});
it('should calculate partial abstract match score', async () => {
// Search with multiple words, some of which might be in abstracts
const results = await searchSpecs('browser security model implementation');
// Should calculate partial match scores
expect(Array.isArray(results)).toBe(true);
});
});
describe('edge cases', () => {
it('should return empty array for very short query', async () => {
// Queries with only very short words (≤2 chars) won't match via word-based matching
const results = await searchSpecs('xy');
// Should still return results if there's a shortname or title match
expect(Array.isArray(results)).toBe(true);
});
it('should handle unicode characters in query', async () => {
const results = await searchSpecs('日本語');
expect(Array.isArray(results)).toBe(true);
// Unicode query unlikely to match anything, but should not throw
expect(results.length).toBe(0);
});
it('should handle very long query without error', async () => {
const longQuery = 'a'.repeat(1000);
const results = await searchSpecs(longQuery);
expect(Array.isArray(results)).toBe(true);
});
it('should handle query with special regex characters', async () => {
const results = await searchSpecs('test.*[regex]');
expect(Array.isArray(results)).toBe(true);
});
it('should handle query with only whitespace', async () => {
const results = await searchSpecs(' ');
expect(Array.isArray(results)).toBe(true);
});
it('should handle query with mixed case', async () => {
const results1 = await searchSpecs('FETCH');
const results2 = await searchSpecs('fetch');
// Both should find the same spec (case-insensitive)
expect(results1.length).toBe(results2.length);
if (results1.length > 0) {
expect(results1[0].shortname).toBe(results2[0].shortname);
}
});
it('should handle limit of 0', async () => {
const results = await searchSpecs('web', 0);
// Limit of 0 means slice(0, 0) = empty array
expect(results.length).toBe(0);
});
it('should handle very large limit', async () => {
const results = await searchSpecs('web', 10000);
expect(Array.isArray(results)).toBe(true);
// Should return all matches up to the total number of specs
});
});