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
820 lines
31 KiB
JavaScript
820 lines
31 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Document Library UI Tests
|
|
*
|
|
* Tests for the library page including filters, views, PDF/text viewers,
|
|
* and document management.
|
|
*
|
|
* Run: node test_library_documents_ci.js
|
|
*/
|
|
|
|
const { setupTest, teardownTest, TestResults, log, delay, navigateTo, withTimeout } = require('./test_lib');
|
|
|
|
// ============================================================================
|
|
// Library Page Structure Tests
|
|
// ============================================================================
|
|
const LibraryPageTests = {
|
|
async libraryPageLoads(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/library`);
|
|
|
|
const result = await page.evaluate(() => {
|
|
const hasContent = document.body.textContent.length > 100;
|
|
const title = document.title.toLowerCase();
|
|
const hasLibraryContent = title.includes('library') || title.includes('document') ||
|
|
!!document.querySelector('.library, #library, [class*="library"]');
|
|
|
|
return {
|
|
hasContent,
|
|
hasLibraryContent,
|
|
title,
|
|
url: window.location.href
|
|
};
|
|
});
|
|
|
|
return {
|
|
passed: result.hasContent,
|
|
message: `Library page loads (title: "${result.title}", has content: ${result.hasContent})`
|
|
};
|
|
},
|
|
|
|
async libraryHeaderStats(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/library`);
|
|
|
|
const result = await page.evaluate(() => {
|
|
// Look for statistics in header
|
|
const statsArea = document.querySelector('.stats, .ldr-library-stats, .header-stats, [class*="statistics"]');
|
|
const pdfCount = document.querySelector('#total-pdfs');
|
|
const storageSize = document.querySelector('#total-size');
|
|
|
|
// Look for any numbers that might be stats
|
|
const pageText = document.body.textContent;
|
|
const hasNumbers = /\d+\s*(?:PDFs?|documents?|MB|GB|files?)/i.test(pageText);
|
|
|
|
return {
|
|
hasStatsArea: !!statsArea,
|
|
hasPdfCount: !!pdfCount,
|
|
hasStorageSize: !!storageSize,
|
|
hasNumbers,
|
|
pdfCountText: pdfCount?.textContent?.trim(),
|
|
storageSizeText: storageSize?.textContent?.trim()
|
|
};
|
|
});
|
|
|
|
const hasStats = result.hasStatsArea || result.hasPdfCount || result.hasNumbers;
|
|
|
|
return {
|
|
passed: hasStats,
|
|
message: `Library stats: area=${result.hasStatsArea}, count="${result.pdfCountText}", size="${result.storageSizeText}"`
|
|
};
|
|
},
|
|
|
|
async storageModeDisplay(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/library`);
|
|
|
|
const result = await page.evaluate(() => {
|
|
const pageText = document.body.textContent.toLowerCase();
|
|
|
|
// Look for storage mode indicators
|
|
const hasEncrypted = pageText.includes('encrypted') || pageText.includes('database');
|
|
const hasFilesystem = pageText.includes('filesystem') || pageText.includes('unencrypted');
|
|
const hasTextOnly = pageText.includes('text only');
|
|
|
|
// Look for storage mode badge/indicator
|
|
const modeBadge = document.querySelector(
|
|
'.storage-mode, ' +
|
|
'[class*="storage-badge"], ' +
|
|
'.mode-indicator, ' +
|
|
'.badge'
|
|
);
|
|
|
|
return {
|
|
hasEncrypted,
|
|
hasFilesystem,
|
|
hasTextOnly,
|
|
hasModeBadge: !!modeBadge,
|
|
badgeText: modeBadge?.textContent?.trim()
|
|
};
|
|
});
|
|
|
|
const hasModeInfo = result.hasEncrypted || result.hasFilesystem || result.hasTextOnly || result.hasModeBadge;
|
|
|
|
if (!hasModeInfo) {
|
|
return { passed: null, skipped: true, message: 'No storage mode display found' };
|
|
}
|
|
|
|
return {
|
|
passed: true,
|
|
message: `Storage mode: encrypted=${result.hasEncrypted}, filesystem=${result.hasFilesystem}, badge="${result.badgeText}"`
|
|
};
|
|
}
|
|
};
|
|
|
|
// ============================================================================
|
|
// Filter Controls Tests
|
|
// ============================================================================
|
|
const FilterControlsTests = {
|
|
async filterControlsExist(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/library`);
|
|
await delay(500);
|
|
|
|
const result = await page.evaluate(() => {
|
|
// Broader search for search inputs
|
|
const searchInput = document.querySelector(
|
|
'input[type="search"], ' +
|
|
'input[type="text"][placeholder*="search" i], ' +
|
|
'input[type="text"][placeholder*="filter" i], ' +
|
|
'#search, ' +
|
|
'.search-input, ' +
|
|
'[class*="search-input"], ' +
|
|
'[class*="filter-input"]'
|
|
);
|
|
|
|
// Look for any filter controls
|
|
const collectionFilter = document.querySelector(
|
|
'select[name*="collection"], ' +
|
|
'#collection-filter, ' +
|
|
'[class*="collection-filter"], ' +
|
|
'[class*="collection-select"]'
|
|
);
|
|
|
|
const domainFilter = document.querySelector(
|
|
'select[name*="domain"], ' +
|
|
'#domain-filter, ' +
|
|
'[class*="domain-filter"]'
|
|
);
|
|
|
|
const dateFilter = document.querySelector(
|
|
'select[name*="date"], ' +
|
|
'#date-filter, ' +
|
|
'.date-filter, ' +
|
|
'[class*="date-range"], ' +
|
|
'[class*="date-filter"]'
|
|
);
|
|
|
|
// Also check if library page has any content at all
|
|
const hasLibraryContent = document.querySelector('.library, #library, [class*="library"], .documents, [class*="document"]');
|
|
const pageTitle = document.title?.toLowerCase() || '';
|
|
const isLibraryPage = pageTitle.includes('library') || !!hasLibraryContent;
|
|
|
|
return {
|
|
hasSearch: !!searchInput,
|
|
hasCollectionFilter: !!collectionFilter,
|
|
hasDomainFilter: !!domainFilter,
|
|
hasDateFilter: !!dateFilter,
|
|
searchPlaceholder: searchInput?.placeholder,
|
|
isLibraryPage,
|
|
hasAnyContent: !!hasLibraryContent
|
|
};
|
|
});
|
|
|
|
const hasFilters = result.hasSearch || result.hasCollectionFilter ||
|
|
result.hasDomainFilter || result.hasDateFilter;
|
|
|
|
// If no filters but we're on library page, skip instead of fail
|
|
if (!hasFilters) {
|
|
if (result.isLibraryPage || result.hasAnyContent) {
|
|
return { passed: null, skipped: true, message: 'Library page found but filter controls not visible (may be dynamic or require documents)' };
|
|
}
|
|
return { passed: null, skipped: true, message: 'Library page or filter controls not found' };
|
|
}
|
|
|
|
return {
|
|
passed: true,
|
|
message: `Filters: search=${result.hasSearch} ("${result.searchPlaceholder}"), collection=${result.hasCollectionFilter}, domain=${result.hasDomainFilter}, date=${result.hasDateFilter}`
|
|
};
|
|
},
|
|
|
|
async searchFilterFunctionality(page, baseUrl) {
|
|
// #search-documents (library.html) is the real document search input,
|
|
// wired to filter .ldr-document-card client-side on the 'input' event
|
|
// (library_search_ui.js:142). The old test typed a query but returned
|
|
// passed:true regardless. Assert the real input exists and reflects
|
|
// typed input.
|
|
await navigateTo(page, `${baseUrl}/library`);
|
|
await page.waitForSelector('#search-documents', { timeout: 10000 });
|
|
|
|
const info = await page.evaluate(() => {
|
|
const el = document.getElementById('search-documents');
|
|
return { exists: !!el, type: el?.type, placeholder: el?.placeholder || '' };
|
|
});
|
|
if (!info.exists) {
|
|
return { passed: false, message: '#search-documents input not found on /library' };
|
|
}
|
|
|
|
// Clear any pre-filled value first so the assertion is deterministic
|
|
// (Puppeteer's page.type appends — there is no page.fill).
|
|
await page.$eval('#search-documents', el => { el.value = ''; });
|
|
await page.type('#search-documents', 'fixture');
|
|
const value = await page.$eval('#search-documents', el => el.value);
|
|
const passed = value === 'fixture';
|
|
return {
|
|
passed,
|
|
message: passed
|
|
? `Document search input accepts input (placeholder="${info.placeholder}")`
|
|
: `Search input did not reflect typed value (got "${value}")`
|
|
};
|
|
},
|
|
|
|
async dateFilterButtons(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/library`);
|
|
|
|
const result = await page.evaluate(() => {
|
|
const buttons = Array.from(document.querySelectorAll('button, .btn, [class*="filter-btn"]'));
|
|
|
|
const dateButtons = {
|
|
today: buttons.find(b => b.textContent?.toLowerCase().includes('today')),
|
|
week: buttons.find(b => b.textContent?.toLowerCase().includes('week')),
|
|
month: buttons.find(b => b.textContent?.toLowerCase().includes('month')),
|
|
all: buttons.find(b => b.textContent?.toLowerCase().includes('all'))
|
|
};
|
|
|
|
return {
|
|
hasToday: !!dateButtons.today,
|
|
hasWeek: !!dateButtons.week,
|
|
hasMonth: !!dateButtons.month,
|
|
hasAll: !!dateButtons.all,
|
|
foundButtons: Object.entries(dateButtons).filter(([_k, v]) => v).map(([k]) => k)
|
|
};
|
|
});
|
|
|
|
const hasDateButtons = result.foundButtons.length > 0;
|
|
|
|
if (!hasDateButtons) {
|
|
return { passed: null, skipped: true, message: 'No date filter buttons found' };
|
|
}
|
|
|
|
return {
|
|
passed: true,
|
|
message: `Date filter buttons: ${result.foundButtons.join(', ')}`
|
|
};
|
|
}
|
|
};
|
|
|
|
// ============================================================================
|
|
// View Toggle Tests
|
|
// ============================================================================
|
|
const ViewToggleTests = {
|
|
async viewToggleExists(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/library`);
|
|
|
|
const result = await page.evaluate(() => {
|
|
const gridBtn = document.querySelector(
|
|
'[data-view="grid"], ' +
|
|
'.grid-view, ' +
|
|
'#grid-view, ' +
|
|
'button[title*="grid"], ' +
|
|
'[class*="grid-toggle"]'
|
|
);
|
|
|
|
const listBtn = document.querySelector(
|
|
'[data-view="list"], ' +
|
|
'.list-view, ' +
|
|
'#list-view, ' +
|
|
'button[title*="list"], ' +
|
|
'[class*="list-toggle"]'
|
|
);
|
|
|
|
const viewToggle = document.querySelector(
|
|
'.ldr-view-toggle, ' +
|
|
'[class*="view-switch"], ' +
|
|
'.view-options'
|
|
);
|
|
|
|
return {
|
|
hasGridBtn: !!gridBtn,
|
|
hasListBtn: !!listBtn,
|
|
hasViewToggle: !!viewToggle,
|
|
gridIcon: gridBtn?.innerHTML?.includes('grid') || gridBtn?.className?.includes('grid'),
|
|
listIcon: listBtn?.innerHTML?.includes('list') || listBtn?.className?.includes('list')
|
|
};
|
|
});
|
|
|
|
const hasToggle = result.hasGridBtn || result.hasListBtn || result.hasViewToggle;
|
|
|
|
if (!hasToggle) {
|
|
return { passed: null, skipped: true, message: 'No view toggle found' };
|
|
}
|
|
|
|
return {
|
|
passed: true,
|
|
message: `View toggle: grid=${result.hasGridBtn}, list=${result.hasListBtn}`
|
|
};
|
|
},
|
|
|
|
async viewToggleFunctionality(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/library`);
|
|
|
|
const result = await page.evaluate(() => {
|
|
const gridBtn = document.querySelector('[data-view="grid"], .grid-view, #grid-view');
|
|
const listBtn = document.querySelector('[data-view="list"], .list-view, #list-view');
|
|
|
|
// Get initial view state
|
|
const container = document.querySelector('.library-content, .documents, .doc-list, main');
|
|
const initialClass = container?.className;
|
|
|
|
// Click list view if grid exists (or vice versa)
|
|
if (listBtn) {
|
|
listBtn.click();
|
|
} else if (gridBtn) {
|
|
gridBtn.click();
|
|
}
|
|
|
|
const afterClass = container?.className;
|
|
|
|
return {
|
|
hasButtons: !!(gridBtn || listBtn),
|
|
classChanged: initialClass !== afterClass,
|
|
initialClass,
|
|
afterClass
|
|
};
|
|
});
|
|
|
|
if (!result.hasButtons) {
|
|
return { passed: null, skipped: true, message: 'No view toggle buttons to test' };
|
|
}
|
|
|
|
return {
|
|
passed: result.hasButtons,
|
|
message: `View toggle: class changed=${result.classChanged}`
|
|
};
|
|
}
|
|
};
|
|
|
|
// ============================================================================
|
|
// Document Card Tests
|
|
// ============================================================================
|
|
const DocumentCardTests = {
|
|
async documentCardsDisplay(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/library`);
|
|
|
|
const result = await page.evaluate(() => {
|
|
const cards = document.querySelectorAll(
|
|
'.ldr-document-card, ' +
|
|
'.doc-card, ' +
|
|
'.library-item, ' +
|
|
'[class*="document-item"], ' +
|
|
'.card'
|
|
);
|
|
|
|
if (cards.length === 0) {
|
|
// Check for empty state
|
|
const emptyState = document.querySelector('.ldr-empty-state, .no-documents, [class*="empty"]');
|
|
return {
|
|
hasCards: false,
|
|
cardCount: 0,
|
|
hasEmptyState: !!emptyState,
|
|
emptyText: emptyState?.textContent?.trim()?.substring(0, 50)
|
|
};
|
|
}
|
|
|
|
const firstCard = cards[0];
|
|
const hasTitle = !!firstCard.querySelector('.title, .ldr-doc-title, h3, h4, [class*="title"]');
|
|
const hasAuthor = !!firstCard.querySelector('.author, [class*="author"]');
|
|
const hasDate = !!firstCard.querySelector('.date, time, [class*="date"]');
|
|
const hasDomain = !!firstCard.querySelector('.domain, .badge, [class*="domain"]');
|
|
|
|
return {
|
|
hasCards: true,
|
|
cardCount: cards.length,
|
|
hasTitle,
|
|
hasAuthor,
|
|
hasDate,
|
|
hasDomain
|
|
};
|
|
});
|
|
|
|
if (!result.hasCards && result.hasEmptyState) {
|
|
return {
|
|
passed: true,
|
|
message: `Empty state shown: "${result.emptyText}"`
|
|
};
|
|
}
|
|
|
|
return {
|
|
passed: result.hasCards,
|
|
message: result.hasCards
|
|
? `${result.cardCount} document cards (title=${result.hasTitle}, author=${result.hasAuthor}, date=${result.hasDate})`
|
|
: 'No document cards found'
|
|
};
|
|
},
|
|
|
|
async documentCardActions(page, baseUrl) {
|
|
// NOTE: deferred from the dead-field audit's PR 3. The real assertion
|
|
// (seed collection + upload a doc, then assert .ldr-action-btn-txt +
|
|
// .ldr-btn-delete-doc on the card) works, but the uploaded doc leaks
|
|
// into this file's later PDF/Text viewer tests (collection deletion
|
|
// doesn't cascade-delete the doc), turning their skips into failures.
|
|
// Needs document-level cleanup or test reordering — tracked as a
|
|
// follow-up so it doesn't destabilise the library shard here.
|
|
await navigateTo(page, `${baseUrl}/library`);
|
|
|
|
const result = await page.evaluate(() => {
|
|
const card = document.querySelector('.ldr-document-card, .doc-card, .library-item, .card');
|
|
if (!card) return { hasCard: false };
|
|
|
|
const viewPdfBtn = card.querySelector('[class*="view-pdf"], button[title*="PDF"], .pdf-btn');
|
|
const viewTextBtn = card.querySelector('[class*="view-text"], button[title*="text"], .text-btn');
|
|
const deleteBtn = card.querySelector('[class*="delete"], .delete-btn, button[title*="delete"]');
|
|
const downloadBtn = card.querySelector('[class*="download"], .download-btn, a[download]');
|
|
|
|
return {
|
|
hasCard: true,
|
|
hasViewPdf: !!viewPdfBtn,
|
|
hasViewText: !!viewTextBtn,
|
|
hasDelete: !!deleteBtn,
|
|
hasDownload: !!downloadBtn
|
|
};
|
|
});
|
|
|
|
if (!result.hasCard) {
|
|
return { passed: null, skipped: true, message: 'No document cards to check actions' };
|
|
}
|
|
|
|
const hasActions = result.hasViewPdf || result.hasViewText || result.hasDelete || result.hasDownload;
|
|
|
|
return {
|
|
passed: hasActions,
|
|
message: `Card actions: viewPdf=${result.hasViewPdf}, viewText=${result.hasViewText}, delete=${result.hasDelete}, download=${result.hasDownload}`
|
|
};
|
|
}
|
|
};
|
|
|
|
// ============================================================================
|
|
// PDF Viewer Tests
|
|
// ============================================================================
|
|
const PdfViewerTests = {
|
|
async pdfViewerModalExists(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/library`);
|
|
|
|
// Only test PDF viewer if there are document cards
|
|
const hasCards = await page.evaluate(() => {
|
|
return !!document.querySelector('.ldr-document-card, .doc-card, .library-item');
|
|
});
|
|
if (!hasCards) {
|
|
return { passed: null, skipped: true, message: 'No documents to test PDF viewer' };
|
|
}
|
|
|
|
// Try to click view PDF button on a card
|
|
const clicked = await page.evaluate(() => {
|
|
const card = document.querySelector('.ldr-document-card, .doc-card, .library-item');
|
|
if (!card) return false;
|
|
const viewBtn = card.querySelector(
|
|
'[class*="view-pdf"], ' +
|
|
'button[title*="PDF"], ' +
|
|
'.pdf-btn, ' +
|
|
'[data-action="view-pdf"]'
|
|
);
|
|
if (viewBtn) {
|
|
viewBtn.click();
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
|
|
if (!clicked) {
|
|
return { passed: null, skipped: true, message: 'No PDF view button on document card' };
|
|
}
|
|
|
|
await delay(500);
|
|
|
|
const result = await page.evaluate(() => {
|
|
const modal = document.querySelector(
|
|
'.modal, ' +
|
|
'.pdf-viewer, ' +
|
|
'[role="dialog"], ' +
|
|
'.viewer-modal, ' +
|
|
'#pdf-modal'
|
|
);
|
|
|
|
const pdfEmbed = document.querySelector('embed[type*="pdf"], iframe[src*="pdf"], object[type*="pdf"]');
|
|
const pdfCanvas = document.querySelector('canvas.pdf-page, .pdf-canvas');
|
|
|
|
return {
|
|
hasModal: !!modal && window.getComputedStyle(modal).display !== 'none',
|
|
hasPdfEmbed: !!pdfEmbed,
|
|
hasPdfCanvas: !!pdfCanvas,
|
|
modalClass: modal?.className
|
|
};
|
|
});
|
|
|
|
const hasPdfViewer = result.hasModal || result.hasPdfEmbed || result.hasPdfCanvas;
|
|
|
|
return {
|
|
passed: hasPdfViewer,
|
|
message: hasPdfViewer
|
|
? `PDF viewer: modal=${result.hasModal}, embed=${result.hasPdfEmbed}, canvas=${result.hasPdfCanvas}`
|
|
: 'PDF viewer modal did not appear'
|
|
};
|
|
},
|
|
|
|
async pdfViewerCloseButton(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/library`);
|
|
|
|
// Only test if there are document cards
|
|
const hasCards = await page.evaluate(() => {
|
|
return !!document.querySelector('.ldr-document-card, .doc-card, .library-item');
|
|
});
|
|
if (!hasCards) {
|
|
return { passed: null, skipped: true, message: 'No documents to test PDF close button' };
|
|
}
|
|
|
|
// Open PDF viewer from a card
|
|
await page.evaluate(() => {
|
|
const card = document.querySelector('.ldr-document-card, .doc-card, .library-item');
|
|
if (!card) return;
|
|
const viewBtn = card.querySelector('[class*="view-pdf"], .pdf-btn, [data-action="view-pdf"]');
|
|
if (viewBtn) viewBtn.click();
|
|
});
|
|
|
|
await delay(500);
|
|
|
|
const result = await page.evaluate(() => {
|
|
const modal = document.querySelector('.modal, .pdf-viewer, [role="dialog"]');
|
|
if (!modal) return { hasModal: false };
|
|
|
|
const closeBtn = modal.querySelector(
|
|
'.close, ' +
|
|
'.close-btn, ' +
|
|
'[class*="close"], ' +
|
|
'button[aria-label="Close"], ' +
|
|
'.modal-close'
|
|
);
|
|
|
|
return {
|
|
hasModal: true,
|
|
hasCloseBtn: !!closeBtn,
|
|
closeBtnText: closeBtn?.textContent?.trim() || closeBtn?.getAttribute('aria-label')
|
|
};
|
|
});
|
|
|
|
if (!result.hasModal) {
|
|
return { passed: null, skipped: true, message: 'PDF modal not open for close button test' };
|
|
}
|
|
|
|
return {
|
|
passed: result.hasCloseBtn,
|
|
message: result.hasCloseBtn
|
|
? `Close button found: "${result.closeBtnText}"`
|
|
: 'No close button in PDF viewer'
|
|
};
|
|
}
|
|
};
|
|
|
|
// ============================================================================
|
|
// Text Viewer Tests
|
|
// ============================================================================
|
|
const TextViewerTests = {
|
|
async textViewerModalExists(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/library`);
|
|
|
|
// Only test if there are document cards
|
|
const hasCards = await page.evaluate(() => {
|
|
return !!document.querySelector('.ldr-document-card, .doc-card, .library-item');
|
|
});
|
|
if (!hasCards) {
|
|
return { passed: null, skipped: true, message: 'No documents to test text viewer' };
|
|
}
|
|
|
|
// Try to click view text button on a card
|
|
const clicked = await page.evaluate(() => {
|
|
const card = document.querySelector('.ldr-document-card, .doc-card, .library-item');
|
|
if (!card) return false;
|
|
const viewBtn = card.querySelector(
|
|
'[class*="view-text"], ' +
|
|
'button[title*="text"], ' +
|
|
'.text-btn, ' +
|
|
'[data-action="view-text"]'
|
|
);
|
|
if (viewBtn) {
|
|
viewBtn.click();
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
|
|
if (!clicked) {
|
|
return { passed: null, skipped: true, message: 'No text view button on document card' };
|
|
}
|
|
|
|
await delay(500);
|
|
|
|
const result = await page.evaluate(() => {
|
|
const modal = document.querySelector(
|
|
'.modal, ' +
|
|
'.text-viewer, ' +
|
|
'[role="dialog"], ' +
|
|
'.viewer-modal, ' +
|
|
'#text-modal'
|
|
);
|
|
|
|
const textContent = modal?.querySelector('pre, .text-content, .document-text, textarea[readonly]');
|
|
|
|
return {
|
|
hasModal: !!modal && window.getComputedStyle(modal).display !== 'none',
|
|
hasTextContent: !!textContent,
|
|
textLength: textContent?.textContent?.length || 0
|
|
};
|
|
});
|
|
|
|
const hasTextViewer = result.hasModal || result.hasTextContent;
|
|
|
|
return {
|
|
passed: hasTextViewer,
|
|
message: hasTextViewer
|
|
? `Text viewer: modal=${result.hasModal}, content length=${result.textLength}`
|
|
: 'Text viewer modal did not appear'
|
|
};
|
|
},
|
|
|
|
async textViewerCopyButton(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/library`);
|
|
|
|
// Open text viewer
|
|
await page.evaluate(() => {
|
|
const viewBtn = document.querySelector('[class*="view-text"], .text-btn, [data-action="view-text"]');
|
|
if (viewBtn) viewBtn.click();
|
|
});
|
|
|
|
await delay(500);
|
|
|
|
const result = await page.evaluate(() => {
|
|
const modal = document.querySelector('.modal, .text-viewer, [role="dialog"]');
|
|
if (!modal) return { hasModal: false };
|
|
|
|
const copyBtn = modal.querySelector(
|
|
'[class*="copy"], ' +
|
|
'button[title*="copy"], ' +
|
|
'.copy-btn, ' +
|
|
'[data-action="copy"]'
|
|
);
|
|
|
|
return {
|
|
hasModal: true,
|
|
hasCopyBtn: !!copyBtn,
|
|
copyBtnText: copyBtn?.textContent?.trim() || copyBtn?.getAttribute('title')
|
|
};
|
|
});
|
|
|
|
if (!result.hasModal) {
|
|
return { passed: null, skipped: true, message: 'Text modal not open for copy button test' };
|
|
}
|
|
|
|
if (!result.hasCopyBtn) {
|
|
return { passed: null, skipped: true, message: 'No copy button in text viewer' };
|
|
}
|
|
|
|
return {
|
|
passed: true,
|
|
message: `Copy button found: "${result.copyBtnText}"`
|
|
};
|
|
}
|
|
};
|
|
|
|
// ============================================================================
|
|
// Bulk Actions Tests
|
|
// ============================================================================
|
|
const BulkActionsTests = {
|
|
async getAllPdfsButton(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/library`);
|
|
|
|
const result = await page.evaluate(() => {
|
|
const buttons = Array.from(document.querySelectorAll('button, .btn, a.btn'));
|
|
const getAllBtn = buttons.find(btn => {
|
|
const text = btn.textContent?.toLowerCase() || '';
|
|
return text.includes('get all') || text.includes('download all') ||
|
|
text.includes('fetch all') || text.includes('bulk download');
|
|
});
|
|
|
|
return {
|
|
hasButton: !!getAllBtn,
|
|
buttonText: getAllBtn?.textContent?.trim()
|
|
};
|
|
});
|
|
|
|
if (!result.hasButton) {
|
|
return { passed: null, skipped: true, message: 'No get all PDFs button found' };
|
|
}
|
|
|
|
return {
|
|
passed: true,
|
|
message: `Get all PDFs button: "${result.buttonText}"`
|
|
};
|
|
},
|
|
|
|
async syncLibraryButton(page, baseUrl) {
|
|
await navigateTo(page, `${baseUrl}/library`);
|
|
|
|
const result = await page.evaluate(() => {
|
|
const buttons = Array.from(document.querySelectorAll('button, .btn, a.btn'));
|
|
const syncBtn = buttons.find(btn => {
|
|
const text = btn.textContent?.toLowerCase() || '';
|
|
return text.includes('sync') || text.includes('refresh') || text.includes('update library');
|
|
});
|
|
|
|
return {
|
|
hasButton: !!syncBtn,
|
|
buttonText: syncBtn?.textContent?.trim()
|
|
};
|
|
});
|
|
|
|
if (!result.hasButton) {
|
|
return { passed: null, skipped: true, message: 'No sync library button found' };
|
|
}
|
|
|
|
return {
|
|
passed: true,
|
|
message: `Sync button: "${result.buttonText}"`
|
|
};
|
|
}
|
|
};
|
|
|
|
// ============================================================================
|
|
// Main Test Runner
|
|
// ============================================================================
|
|
async function main() {
|
|
log.section('Document Library UI Tests');
|
|
|
|
const ctx = await setupTest({ authenticate: true });
|
|
const results = new TestResults('Library Documents Tests');
|
|
const { page } = ctx;
|
|
const { baseUrl } = ctx.config;
|
|
|
|
// Helper: run a sub-test with a 30s timeout so a single hang
|
|
// doesn't block the entire 300s process-level timeout.
|
|
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 Structure
|
|
log.section('Library Page Structure');
|
|
await run('Page', 'Loads', (p, u) => LibraryPageTests.libraryPageLoads(p, u));
|
|
await run('Page', 'Header Stats', (p, u) => LibraryPageTests.libraryHeaderStats(p, u));
|
|
await run('Page', 'Storage Mode', (p, u) => LibraryPageTests.storageModeDisplay(p, u));
|
|
|
|
// Filter Controls
|
|
log.section('Filter Controls');
|
|
await run('Filters', 'Controls Exist', (p, u) => FilterControlsTests.filterControlsExist(p, u));
|
|
await run('Filters', 'Search', (p, u) => FilterControlsTests.searchFilterFunctionality(p, u));
|
|
await run('Filters', 'Date Buttons', (p, u) => FilterControlsTests.dateFilterButtons(p, u));
|
|
|
|
// View Toggle
|
|
log.section('View Toggle');
|
|
await run('View', 'Toggle Exists', (p, u) => ViewToggleTests.viewToggleExists(p, u));
|
|
await run('View', 'Toggle Works', (p, u) => ViewToggleTests.viewToggleFunctionality(p, u));
|
|
|
|
// Document Cards
|
|
log.section('Document Cards');
|
|
await run('Cards', 'Display', (p, u) => DocumentCardTests.documentCardsDisplay(p, u));
|
|
await run('Cards', 'Actions', (p, u) => DocumentCardTests.documentCardActions(p, u));
|
|
|
|
// PDF Viewer
|
|
log.section('PDF Viewer');
|
|
await run('PDF', 'Viewer Modal', (p, u) => PdfViewerTests.pdfViewerModalExists(p, u));
|
|
await run('PDF', 'Close Button', (p, u) => PdfViewerTests.pdfViewerCloseButton(p, u));
|
|
|
|
// Text Viewer
|
|
log.section('Text Viewer');
|
|
await run('Text', 'Viewer Modal', (p, u) => TextViewerTests.textViewerModalExists(p, u));
|
|
await run('Text', 'Copy Button', (p, u) => TextViewerTests.textViewerCopyButton(p, u));
|
|
|
|
// Bulk Actions
|
|
log.section('Bulk Actions');
|
|
await run('Bulk', 'Get All PDFs', (p, u) => BulkActionsTests.getAllPdfsButton(p, u));
|
|
await run('Bulk', 'Sync Library', (p, u) => BulkActionsTests.syncLibraryButton(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, FilterControlsTests, ViewToggleTests, DocumentCardTests, PdfViewerTests, TextViewerTests, BulkActionsTests };
|