7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
274 lines
10 KiB
JavaScript
274 lines
10 KiB
JavaScript
/**
|
|
* Tests for components/library_search.js
|
|
*
|
|
* Tests the LibrarySearch utilities:
|
|
* fileTypeIcon, initLibrarySearch, getIndexedCollectionIds,
|
|
* getDefaultCollectionId, searchAllCollections (deduplication logic),
|
|
* renderSemanticResults.
|
|
*/
|
|
|
|
// Stub URLBuilder before loading the module
|
|
window.URLBuilder = {
|
|
build: (template, id) => template.replace('{id}', id),
|
|
documentPage: (id) => `/library/document/${id}`,
|
|
};
|
|
window.URLS = {
|
|
LIBRARY_API: {
|
|
COLLECTION_SEARCH: '/library/api/collections/{id}/search',
|
|
},
|
|
};
|
|
|
|
import '@js/components/library_search.js';
|
|
|
|
const LS = window.LibrarySearch;
|
|
|
|
describe('LibrarySearch', () => {
|
|
describe('initLibrarySearch', () => {
|
|
it('stores default collection ID', () => {
|
|
LS.initLibrarySearch('default-coll', []);
|
|
expect(LS.getDefaultCollectionId()).toBe('default-coll');
|
|
});
|
|
|
|
it('stores collections data', () => {
|
|
LS.initLibrarySearch(null, [
|
|
{ id: 'a', indexed_document_count: 5 },
|
|
{ id: 'b', indexed_document_count: 0 },
|
|
{ id: 'c', indexed_document_count: 10 },
|
|
]);
|
|
expect(LS.getIndexedCollectionIds()).toEqual(['a', 'c']);
|
|
});
|
|
|
|
it('handles null/undefined args', () => {
|
|
LS.initLibrarySearch(null, null);
|
|
expect(LS.getDefaultCollectionId()).toBeNull();
|
|
expect(LS.getIndexedCollectionIds()).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('getIndexedCollectionIds', () => {
|
|
it('filters out collections with 0 indexed documents', () => {
|
|
LS.initLibrarySearch(null, [
|
|
{ id: 'a', indexed_document_count: 5 },
|
|
{ id: 'b', indexed_document_count: 0 },
|
|
{ id: 'c' }, // missing field — treated as 0
|
|
]);
|
|
expect(LS.getIndexedCollectionIds()).toEqual(['a']);
|
|
});
|
|
|
|
it('returns empty array for no collections', () => {
|
|
LS.initLibrarySearch(null, []);
|
|
expect(LS.getIndexedCollectionIds()).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('LIBRARY_CARD_CONFIG', () => {
|
|
const config = LS.getLibraryCardConfig();
|
|
|
|
it('getId returns document_id', () => {
|
|
expect(config.getId({ document_id: 'doc-1' })).toBe('doc-1');
|
|
expect(config.getId({})).toBe('');
|
|
});
|
|
|
|
it('getTitle returns title or "Untitled"', () => {
|
|
expect(config.getTitle({ title: 'My Doc' })).toBe('My Doc');
|
|
expect(config.getTitle({})).toBe('Untitled');
|
|
});
|
|
|
|
it('getUrl builds document page URL', () => {
|
|
expect(config.getUrl({ document_id: 'abc' })).toBe('/library/document/abc');
|
|
});
|
|
|
|
it('getUrl returns # for missing document_id', () => {
|
|
expect(config.getUrl({})).toBe('#');
|
|
});
|
|
|
|
it('getBadges returns file type badge', () => {
|
|
const badges = config.getBadges({ file_type: 'pdf' });
|
|
expect(badges).toHaveLength(1);
|
|
expect(badges[0].label).toBe('PDF');
|
|
expect(badges[0].icon).toBe('file-pdf');
|
|
});
|
|
|
|
it('getBadges returns default DOC badge when no file type', () => {
|
|
const badges = config.getBadges({});
|
|
expect(badges[0].label).toBe('DOC');
|
|
expect(badges[0].icon).toBe('file');
|
|
});
|
|
|
|
it('getBadges maps known file types to icons', () => {
|
|
expect(config.getBadges({ file_type: 'txt' })[0].icon).toBe('file-alt');
|
|
expect(config.getBadges({ file_type: 'md' })[0].icon).toBe('file-code');
|
|
expect(config.getBadges({ file_type: 'markdown' })[0].icon).toBe('file-code');
|
|
expect(config.getBadges({ file_type: 'html' })[0].icon).toBe('file-code');
|
|
});
|
|
|
|
it('getDate returns created_at', () => {
|
|
expect(config.getDate({ created_at: '2025-01-15' })).toBe('2025-01-15');
|
|
expect(config.getDate({})).toBeNull();
|
|
});
|
|
|
|
it('getSubtitle returns domain', () => {
|
|
expect(config.getSubtitle({ domain: 'example.com' })).toBe('example.com');
|
|
expect(config.getSubtitle({})).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('searchAllCollections deduplication', () => {
|
|
let originalFetch;
|
|
|
|
beforeEach(() => {
|
|
originalFetch = globalThis.fetch;
|
|
});
|
|
|
|
afterEach(() => {
|
|
globalThis.fetch = originalFetch;
|
|
});
|
|
|
|
it('returns empty array for empty collection list', async () => {
|
|
const result = await LS.searchAllCollections([], 'query');
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it('deduplicates results by document_id, keeping highest similarity', async () => {
|
|
// Mock fetch to return overlapping results from two collections
|
|
globalThis.fetch = vi.fn()
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: () => Promise.resolve({
|
|
success: true,
|
|
results: [
|
|
{ document_id: 'doc1', similarity: 0.5 },
|
|
{ document_id: 'doc2', similarity: 0.9 },
|
|
],
|
|
}),
|
|
})
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: () => Promise.resolve({
|
|
success: true,
|
|
results: [
|
|
{ document_id: 'doc1', similarity: 0.8 }, // higher than first
|
|
{ document_id: 'doc3', similarity: 0.6 },
|
|
],
|
|
}),
|
|
});
|
|
|
|
const result = await LS.searchAllCollections(['c1', 'c2'], 'test');
|
|
// Should have 3 unique docs
|
|
expect(result).toHaveLength(3);
|
|
// doc1 should have similarity 0.8 (the higher one)
|
|
const doc1 = result.find(r => r.document_id === 'doc1');
|
|
expect(doc1.similarity).toBe(0.8);
|
|
});
|
|
|
|
it('sorts results by similarity descending', async () => {
|
|
globalThis.fetch = vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve({
|
|
success: true,
|
|
results: [
|
|
{ document_id: 'a', similarity: 0.3 },
|
|
{ document_id: 'b', similarity: 0.9 },
|
|
{ document_id: 'c', similarity: 0.6 },
|
|
],
|
|
}),
|
|
});
|
|
|
|
const result = await LS.searchAllCollections(['c1'], 'test');
|
|
expect(result.map(r => r.document_id)).toEqual(['b', 'c', 'a']);
|
|
});
|
|
|
|
it('handles failed search responses gracefully', async () => {
|
|
globalThis.fetch = vi.fn()
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: () => Promise.resolve({ success: false }),
|
|
})
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: () => Promise.resolve({
|
|
success: true,
|
|
results: [{ document_id: 'd1', similarity: 0.7 }],
|
|
}),
|
|
});
|
|
|
|
const result = await LS.searchAllCollections(['c1', 'c2'], 'test');
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].document_id).toBe('d1');
|
|
});
|
|
|
|
it('catches per-collection fetch errors', async () => {
|
|
globalThis.fetch = vi.fn()
|
|
.mockRejectedValueOnce(new Error('network'))
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: () => Promise.resolve({
|
|
success: true,
|
|
results: [{ document_id: 'd1', similarity: 0.5 }],
|
|
}),
|
|
});
|
|
|
|
const result = await LS.searchAllCollections(['c1', 'c2'], 'test');
|
|
expect(result).toHaveLength(1);
|
|
});
|
|
|
|
it('respects limit parameter', async () => {
|
|
globalThis.fetch = vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve({
|
|
success: true,
|
|
results: Array.from({ length: 30 }, (_, i) => ({
|
|
document_id: `doc${i}`,
|
|
similarity: 1 - i * 0.01,
|
|
})),
|
|
}),
|
|
});
|
|
|
|
const result = await LS.searchAllCollections(['c1'], 'test', 5);
|
|
expect(result).toHaveLength(5);
|
|
});
|
|
|
|
it('skips results without document_id', async () => {
|
|
globalThis.fetch = vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve({
|
|
success: true,
|
|
results: [
|
|
{ document_id: 'a', similarity: 0.5 },
|
|
{ similarity: 0.9 }, // missing document_id
|
|
],
|
|
}),
|
|
});
|
|
|
|
const result = await LS.searchAllCollections(['c1'], 'test');
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].document_id).toBe('a');
|
|
});
|
|
});
|
|
|
|
describe('renderSemanticResults', () => {
|
|
it('renders empty state when no results', () => {
|
|
const container = document.createElement('div');
|
|
LS.renderSemanticResults([], container, 'query');
|
|
expect(container.querySelector('.ldr-empty-state')).not.toBeNull();
|
|
});
|
|
|
|
it('renders empty state for null results', () => {
|
|
const container = document.createElement('div');
|
|
LS.renderSemanticResults(null, container, 'query');
|
|
expect(container.querySelector('.ldr-empty-state')).not.toBeNull();
|
|
});
|
|
|
|
it('does nothing when container is null', () => {
|
|
expect(() => LS.renderSemanticResults([], null, 'q')).not.toThrow();
|
|
});
|
|
|
|
it('renders fallback message when SemanticSearch module not loaded', () => {
|
|
const container = document.createElement('div');
|
|
// SemanticSearch not loaded in tests
|
|
LS.renderSemanticResults([{ document_id: '1' }], container, 'q');
|
|
expect(container.textContent).toContain('Search module not loaded');
|
|
});
|
|
});
|
|
});
|