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
463 lines
19 KiB
JavaScript
463 lines
19 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Library & Collections UI Tests
|
|
*
|
|
* Tests for the library page, collections management, and document viewing.
|
|
*
|
|
* Run: node test_library_collections_ci.js
|
|
*/
|
|
const { setupTest, teardownTest, TestResults, log, delay, navigateTo, withTimeout } = require('./test_lib');
|
|
const { seedCollection, deleteCollection } = require('./test_lib/fixtures');
|
|
|
|
// ============================================================================
|
|
// Library Page Tests
|
|
// ============================================================================
|
|
const LibraryPageTests = {
|
|
async libraryPageLoads(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/library`);
|
|
|
|
const result = await page.evaluate(() => {
|
|
return {
|
|
hasLibraryContent: !!document.querySelector('.ldr-library-container, .ldr-library, #library, .document-list'),
|
|
hasHeader: !!document.querySelector('h1, .ldr-library-header, .page-title'),
|
|
headerText: document.querySelector('h1, .ldr-library-header, .page-title')?.textContent?.trim(),
|
|
hasDocuments: document.querySelectorAll('.document-item, .library-item, tr[data-id], .ldr-document').length,
|
|
hasTable: !!document.querySelector('table, .document-table')
|
|
};
|
|
});
|
|
|
|
const passed = result.hasLibraryContent || result.hasHeader || result.hasTable;
|
|
return {
|
|
passed,
|
|
message: passed
|
|
? `Library page loaded (header: "${result.headerText}", documents: ${result.hasDocuments})`
|
|
: 'Library page failed to load'
|
|
};
|
|
},
|
|
|
|
async libraryHeaderButtons(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/library`);
|
|
|
|
const result = await page.evaluate(() => {
|
|
const buttons = Array.from(document.querySelectorAll('button, a.btn, .btn'));
|
|
const buttonTexts = buttons.map(b => b.textContent?.toLowerCase() || '');
|
|
|
|
return {
|
|
hasSyncButton: buttonTexts.some(t => t.includes('sync')),
|
|
hasGetPdfsButton: buttonTexts.some(t => t.includes('pdf') || t.includes('download')),
|
|
hasTextOnlyButton: buttonTexts.some(t => t.includes('text') || t.includes('extract')),
|
|
hasUploadButton: buttonTexts.some(t => t.includes('upload')),
|
|
buttonCount: buttons.length,
|
|
foundButtons: buttonTexts.filter(t => t.length > 0 && t.length < 50).slice(0, 8)
|
|
};
|
|
});
|
|
|
|
const hasAnyButton = result.hasSyncButton || result.hasGetPdfsButton || result.hasUploadButton;
|
|
return {
|
|
passed: hasAnyButton,
|
|
message: hasAnyButton
|
|
? `Library buttons found: sync=${result.hasSyncButton}, pdfs=${result.hasGetPdfsButton}, upload=${result.hasUploadButton}`
|
|
: `No expected buttons found. Found: ${result.foundButtons.join(', ')}`
|
|
};
|
|
},
|
|
|
|
async storageModeBadge(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/library`);
|
|
|
|
const result = await page.evaluate(() => {
|
|
const badge = document.querySelector(
|
|
'.storage-mode, ' +
|
|
'.badge[class*="storage"], ' +
|
|
'[data-storage-mode], ' +
|
|
'.ldr-storage-badge'
|
|
);
|
|
|
|
const storageInfo = document.querySelector('.storage-info, .ldr-library-stats, .storage-status');
|
|
|
|
return {
|
|
hasBadge: !!badge,
|
|
badgeText: badge?.textContent?.trim(),
|
|
hasStorageInfo: !!storageInfo,
|
|
storageInfoText: storageInfo?.textContent?.trim().substring(0, 100)
|
|
};
|
|
});
|
|
|
|
if (!result.hasBadge && !result.hasStorageInfo) {
|
|
return { passed: null, skipped: true, message: 'No storage mode indicator found' };
|
|
}
|
|
|
|
return {
|
|
passed: true,
|
|
message: result.hasBadge
|
|
? `Storage mode badge: "${result.badgeText}"`
|
|
: `Storage info displayed: "${result.storageInfoText}"`
|
|
};
|
|
},
|
|
|
|
async collectionFilterDropdown(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/library`);
|
|
|
|
const result = await page.evaluate(() => {
|
|
const collectionFilter = document.querySelector(
|
|
'select[name*="collection"], ' +
|
|
'#collection-filter, ' +
|
|
'.collection-filter, ' +
|
|
'select.ldr-filter'
|
|
);
|
|
|
|
if (!collectionFilter) return { exists: false };
|
|
|
|
const options = Array.from(collectionFilter.options);
|
|
return {
|
|
exists: true,
|
|
optionCount: options.length,
|
|
options: options.map(o => o.text).slice(0, 8)
|
|
};
|
|
});
|
|
|
|
if (!result.exists) {
|
|
return { passed: null, skipped: true, message: 'No collection filter dropdown found' };
|
|
}
|
|
|
|
return {
|
|
passed: result.optionCount > 0,
|
|
message: `Collection filter has ${result.optionCount} options: ${result.options.join(', ')}`
|
|
};
|
|
},
|
|
|
|
async statusFilterDropdown(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/library`);
|
|
|
|
const result = await page.evaluate(() => {
|
|
const statusFilter = document.querySelector(
|
|
'select[name*="status"], ' +
|
|
'#status-filter, ' +
|
|
'.status-filter'
|
|
);
|
|
|
|
if (!statusFilter) return { exists: false };
|
|
|
|
const options = Array.from(statusFilter.options);
|
|
return {
|
|
exists: true,
|
|
optionCount: options.length,
|
|
options: options.map(o => o.text).slice(0, 8)
|
|
};
|
|
});
|
|
|
|
if (!result.exists) {
|
|
return { passed: null, skipped: true, message: 'No status filter dropdown found' };
|
|
}
|
|
|
|
return {
|
|
passed: result.optionCount > 0,
|
|
message: `Status filter has ${result.optionCount} options: ${result.options.join(', ')}`
|
|
};
|
|
}
|
|
};
|
|
|
|
// ============================================================================
|
|
// Collections Page Tests
|
|
// ============================================================================
|
|
const CollectionsPageTests = {
|
|
async collectionsPageLoads(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/library/collections`);
|
|
|
|
const result = await page.evaluate(() => {
|
|
return {
|
|
hasContent: !!document.querySelector('.collections-container, .ldr-collections, #collections, .collection-list'),
|
|
hasHeader: !!document.querySelector('h1, .collections-header, .page-title'),
|
|
headerText: document.querySelector('h1, .collections-header, .page-title')?.textContent?.trim(),
|
|
collectionCount: document.querySelectorAll('.collection-card, .collection-item, [data-collection-id]').length
|
|
};
|
|
});
|
|
|
|
const passed = result.hasContent || result.hasHeader;
|
|
return {
|
|
passed,
|
|
message: passed
|
|
? `Collections page loaded (header: "${result.headerText}", collections: ${result.collectionCount})`
|
|
: 'Collections page failed to load'
|
|
};
|
|
},
|
|
|
|
async createCollectionButton(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/library/collections`);
|
|
|
|
const { found, href } = await page.evaluate(() => {
|
|
const a = document.querySelector('a#create-collection-btn[href*="/library/collections/create"]');
|
|
return { found: !!a, href: a?.getAttribute('href') };
|
|
});
|
|
|
|
return {
|
|
passed: found,
|
|
message: found
|
|
? `Create collection anchor found (href: "${href}")`
|
|
: 'No create collection anchor found'
|
|
};
|
|
},
|
|
|
|
async collectionCardStructure(page, baseUrl) {
|
|
// Collections render client-side (collections_manager.js) into
|
|
// #collections-container as
|
|
// <div class="ldr-collection-card-wrapper" data-id="<id>"> holding the
|
|
// clickable <a class="ldr-collection-card"> (with .ldr-collection-header
|
|
// h3 (name) + .ldr-collection-stats) plus a sibling actions row with the
|
|
// Reindex button + .ldr-collection-view-link. The per-collection data-id
|
|
// lives on the wrapper, not the card <a>. The old test scanned for
|
|
// `.collection-card` (no such class exists) and SKIPped on an empty DB.
|
|
// Seed a collection and assert the real card markup, then clean up.
|
|
await navigateTo(page, `${baseUrl}/library/collections`);
|
|
const seeded = await seedCollection(page);
|
|
if (!seeded) {
|
|
return { passed: false, message: 'Could not seed collection for card-structure test' };
|
|
}
|
|
try {
|
|
// navigateTo no-ops (already on this path); reload to render the new card.
|
|
await page.reload({ waitUntil: 'domcontentloaded' });
|
|
await page.waitForSelector(`.ldr-collection-card-wrapper[data-id="${seeded.id}"]`, { timeout: 10000 });
|
|
|
|
const result = await page.evaluate((id) => {
|
|
const card = document.querySelector(`.ldr-collection-card-wrapper[data-id="${id}"]`);
|
|
if (!card) return { found: false };
|
|
return {
|
|
found: true,
|
|
name: card.querySelector('.ldr-collection-header h3')?.textContent?.trim(),
|
|
hasStats: !!card.querySelector('.ldr-collection-stats'),
|
|
hasViewLink: !!card.querySelector('.ldr-collection-view-link'),
|
|
};
|
|
}, seeded.id);
|
|
|
|
const passed = result.found && result.name === seeded.name && result.hasStats && result.hasViewLink;
|
|
return {
|
|
passed,
|
|
message: passed
|
|
? `Collection card renders real structure (name="${result.name}", stats + view-link present)`
|
|
: `Collection card structure incomplete (found=${result.found}, name="${result.name}", stats=${result.hasStats}, viewLink=${result.hasViewLink})`
|
|
};
|
|
} finally {
|
|
await deleteCollection(page, seeded.id);
|
|
}
|
|
},
|
|
|
|
async uploadDocumentButton(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/library/collections`);
|
|
|
|
const result = await page.evaluate(() => {
|
|
const uploadBtn = document.querySelector(
|
|
'button[onclick*="upload"], ' +
|
|
'a[href*="upload"], ' +
|
|
'.upload-btn, ' +
|
|
'input[type="file"]'
|
|
);
|
|
|
|
const buttons = Array.from(document.querySelectorAll('button, a.btn'));
|
|
const uploadByText = buttons.find(b => b.textContent?.toLowerCase().includes('upload'));
|
|
|
|
return {
|
|
hasUploadButton: !!uploadBtn || !!uploadByText,
|
|
buttonText: (uploadBtn || uploadByText)?.textContent?.trim(),
|
|
hasFileInput: !!document.querySelector('input[type="file"]')
|
|
};
|
|
});
|
|
|
|
if (!result.hasUploadButton && !result.hasFileInput) {
|
|
return { passed: null, skipped: true, message: 'No upload functionality found on collections page' };
|
|
}
|
|
|
|
return {
|
|
passed: true,
|
|
message: result.hasUploadButton
|
|
? `Upload button found ("${result.buttonText}")`
|
|
: 'File input found for uploads'
|
|
};
|
|
}
|
|
};
|
|
|
|
// ============================================================================
|
|
// Document Details Tests
|
|
// ============================================================================
|
|
const DocumentTests = {
|
|
async documentDetailsPage(page, baseUrl) {
|
|
// First check if there are any documents
|
|
await navigateTo(page, `${baseUrl}/library`);
|
|
|
|
const docLink = await page.evaluate(() => {
|
|
const link = document.querySelector('a[href*="/documents/"], a[href*="/document/"]');
|
|
return link?.href;
|
|
});
|
|
|
|
if (!docLink) {
|
|
return { passed: null, skipped: true, message: 'No documents to test details page' };
|
|
}
|
|
|
|
await navigateTo(page, docLink);
|
|
|
|
const result = await page.evaluate(() => {
|
|
return {
|
|
hasTitle: !!document.querySelector('h1, .document-title, .page-title'),
|
|
titleText: document.querySelector('h1, .document-title, .page-title')?.textContent?.trim(),
|
|
hasMetadata: !!document.querySelector('.metadata, .document-info, .details'),
|
|
hasContent: !!document.querySelector('.document-content, .text-content, .content, pre, .markdown'),
|
|
hasBackButton: !!document.querySelector('a[href*="/library"], .back-btn, .btn-back')
|
|
};
|
|
});
|
|
|
|
const passed = result.hasTitle || result.hasContent;
|
|
return {
|
|
passed,
|
|
message: passed
|
|
? `Document details page loaded (title: "${result.titleText?.substring(0, 30)}", metadata=${result.hasMetadata}, content=${result.hasContent})`
|
|
: 'Document details page missing expected elements'
|
|
};
|
|
}
|
|
};
|
|
|
|
// ============================================================================
|
|
// Library API Tests
|
|
// ============================================================================
|
|
const LibraryApiTests = {
|
|
async libraryApiResponds(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/library`);
|
|
|
|
const result = await page.evaluate(async (url) => {
|
|
try {
|
|
// Try common library API endpoints
|
|
const endpoints = [
|
|
'/library/api/documents',
|
|
'/library/api/collections',
|
|
'/library/api/collections/list'
|
|
];
|
|
|
|
for (const endpoint of endpoints) {
|
|
try {
|
|
const response = await fetch(`${url}${endpoint}`);
|
|
if (response.ok) {
|
|
return { ok: true, endpoint, status: response.status };
|
|
}
|
|
} catch {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
return { ok: false, error: 'No library API endpoint responded' };
|
|
} catch (e) {
|
|
return { ok: false, error: e.message };
|
|
}
|
|
}, baseUrl);
|
|
|
|
return {
|
|
passed: result.ok,
|
|
message: result.ok
|
|
? `Library API responds at ${result.endpoint} (status ${result.status})`
|
|
: `Library API check: ${result.error}`
|
|
};
|
|
},
|
|
|
|
async collectionsApiResponds(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/library/collections`);
|
|
|
|
const result = await page.evaluate(async (url) => {
|
|
try {
|
|
const response = await fetch(`${url}/library/api/collections`);
|
|
if (!response.ok) return { ok: false, status: response.status };
|
|
|
|
const data = await response.json();
|
|
return {
|
|
ok: true,
|
|
status: response.status,
|
|
collectionCount: Array.isArray(data) ? data.length : Object.keys(data).length
|
|
};
|
|
} catch (e) {
|
|
return { ok: false, error: e.message };
|
|
}
|
|
}, baseUrl);
|
|
|
|
if (!result.ok && result.status === 404) {
|
|
return { passed: null, skipped: true, message: 'Collections API endpoint not found' };
|
|
}
|
|
|
|
return {
|
|
passed: result.ok,
|
|
message: result.ok
|
|
? `Collections API responds (${result.collectionCount} collections)`
|
|
: `Collections API failed: ${result.error || 'status ' + result.status}`
|
|
};
|
|
}
|
|
};
|
|
|
|
// ============================================================================
|
|
// Main Test Runner
|
|
// ============================================================================
|
|
async function main() {
|
|
log.section('Library & Collections Tests');
|
|
|
|
const ctx = await setupTest({ authenticate: true });
|
|
const results = new TestResults('Library & Collections Tests');
|
|
const { page } = ctx;
|
|
const { baseUrl } = ctx.config;
|
|
|
|
const subTestTimeout = ctx.config.isCI ? 60000 : 30000;
|
|
async function run(category, name, testFn) {
|
|
try {
|
|
const result = await withTimeout(
|
|
testFn(page, baseUrl),
|
|
subTestTimeout,
|
|
`${category}/${name}`
|
|
);
|
|
if (result.skipped) {
|
|
results.skip(category, name, result.message);
|
|
} else {
|
|
results.add(category, name, result.passed, result.message);
|
|
}
|
|
} catch (error) {
|
|
results.add(category, name, false, `Error: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
try {
|
|
// Library Page Tests
|
|
log.section('Library Page');
|
|
await run('Library', 'Library Page Loads', (p, u) => LibraryPageTests.libraryPageLoads(p, u));
|
|
await run('Library', 'Library Header Buttons', (p, u) => LibraryPageTests.libraryHeaderButtons(p, u));
|
|
await run('Library', 'Storage Mode Badge', (p, u) => LibraryPageTests.storageModeBadge(p, u));
|
|
await run('Library', 'Collection Filter Dropdown', (p, u) => LibraryPageTests.collectionFilterDropdown(p, u));
|
|
await run('Library', 'Status Filter Dropdown', (p, u) => LibraryPageTests.statusFilterDropdown(p, u));
|
|
|
|
// Collections Page Tests
|
|
log.section('Collections Page');
|
|
await run('Collections', 'Collections Page Loads', (p, u) => CollectionsPageTests.collectionsPageLoads(p, u));
|
|
await run('Collections', 'Create Collection Button', (p, u) => CollectionsPageTests.createCollectionButton(p, u));
|
|
await run('Collections', 'Collection Card Structure', (p, u) => CollectionsPageTests.collectionCardStructure(p, u));
|
|
await run('Collections', 'Upload Document Button', (p, u) => CollectionsPageTests.uploadDocumentButton(p, u));
|
|
|
|
// Document Details Tests
|
|
log.section('Document Details');
|
|
await run('Documents', 'Document Details Page', (p, u) => DocumentTests.documentDetailsPage(p, u));
|
|
|
|
// API Tests
|
|
log.section('Library APIs');
|
|
await run('API', 'Library API Responds', (p, u) => LibraryApiTests.libraryApiResponds(p, u));
|
|
await run('API', 'Collections API Responds', (p, u) => LibraryApiTests.collectionsApiResponds(p, u));
|
|
|
|
} catch (error) {
|
|
log.error(`Fatal error: ${error.message}`);
|
|
console.error(error.stack);
|
|
} finally {
|
|
results.print();
|
|
results.save();
|
|
await teardownTest(ctx);
|
|
process.exit(results.exitCode());
|
|
}
|
|
}
|
|
|
|
// Run if executed directly
|
|
if (require.main === module) {
|
|
main().catch(error => {
|
|
console.error('Test runner failed:', error);
|
|
process.exit(1);
|
|
});
|
|
}
|
|
|
|
module.exports = { LibraryPageTests, CollectionsPageTests, DocumentTests, LibraryApiTests };
|