Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

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');
});
});
});