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

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