Readur/frontend/e2e/utils/test-helpers.ts

299 lines
11 KiB
TypeScript

import { Page, expect } from '@playwright/test';
import { TEST_FILES } from './test-data';
export class TestHelpers {
constructor(private page: Page) {}
async waitForApiCall(urlPattern: string | RegExp, timeout = 10000) {
return this.page.waitForResponse(resp =>
typeof urlPattern === 'string'
? resp.url().includes(urlPattern)
: urlPattern.test(resp.url()),
{ timeout }
);
}
async uploadFile(inputSelector: string, filePath: string) {
const fileInput = this.page.locator(inputSelector);
await fileInput.setInputFiles(filePath);
}
async clearAndType(selector: string, text: string) {
await this.page.fill(selector, '');
await this.page.type(selector, text);
}
async waitForToast(message?: string) {
const toast = this.page.locator('[data-testid="toast"], .toast, [role="alert"]').first();
await expect(toast).toBeVisible({ timeout: 5000 });
if (message) {
await expect(toast).toContainText(message);
}
return toast;
}
async waitForLoadingToComplete() {
// Wait for any loading spinners to disappear
await this.page.waitForFunction(() =>
!document.querySelector('[data-testid="loading"], .loading, [aria-label*="loading" i]')
);
}
async waitForWebKitStability() {
const browserName = await this.page.evaluate(() => navigator.userAgent);
const isWebKit = browserName.includes('WebKit') && !browserName.includes('Chrome');
if (isWebKit) {
console.log('WebKit stability waiting initiated...');
// Wait for network to be completely idle
await this.page.waitForLoadState('networkidle');
await this.page.waitForTimeout(3000);
// Wait for JavaScript to finish executing
await this.page.waitForFunction(() => {
return document.readyState === 'complete' &&
typeof window !== 'undefined';
}, { timeout: 15000 });
// Extra stability wait
await this.page.waitForTimeout(2000);
console.log('WebKit stability waiting completed');
}
}
async waitForBrowserStability() {
const browserName = await this.page.context().browser()?.browserType().name() || '';
switch (browserName) {
case 'webkit':
await this.waitForWebKitStability();
break;
case 'firefox':
// Firefox-specific stability wait
console.log('Firefox stability waiting initiated...');
await this.page.waitForLoadState('networkidle');
await this.page.waitForTimeout(2000);
// Firefox sometimes needs extra time for form validation
await this.page.waitForFunction(() => {
return document.readyState === 'complete' &&
typeof window !== 'undefined' &&
!document.querySelector('.MuiCircularProgress-root');
}, { timeout: 15000 });
console.log('Firefox stability waiting completed');
break;
default:
// Chromium and others
await this.page.waitForLoadState('networkidle');
await this.page.waitForTimeout(500);
break;
}
}
async navigateToPage(path: string) {
await this.page.goto(path);
await this.waitForLoadingToComplete();
// WebKit-specific stability waiting
const browserName = await this.page.evaluate(() => navigator.userAgent);
const isWebKit = browserName.includes('WebKit') && !browserName.includes('Chrome');
if (isWebKit) {
console.log('WebKit detected - adding stability waiting for page:', path);
// Wait for network to be completely idle
await this.page.waitForLoadState('networkidle');
await this.page.waitForTimeout(3000);
// Wait for JavaScript to finish executing and ensure we're not stuck on login
await this.page.waitForFunction(() => {
return document.readyState === 'complete' &&
typeof window !== 'undefined' &&
!window.location.href.includes('/login') &&
!window.location.pathname.includes('/login');
}, { timeout: 20000 });
// Extra stability wait
await this.page.waitForTimeout(2000);
console.log('WebKit stability waiting completed for:', path);
}
}
async takeScreenshotOnFailure(testName: string) {
await this.page.screenshot({
path: `test-results/screenshots/${testName}-${Date.now()}.png`,
fullPage: true
});
}
async uploadTestDocument(fileName: string = 'test1.png') {
try {
console.log(`Uploading test document: ${fileName}`);
// Navigate to upload page
await this.page.goto('/upload');
await this.waitForLoadingToComplete();
// Look for file input - react-dropzone creates hidden inputs
const fileInput = this.page.locator('input[type="file"]').first();
await expect(fileInput).toBeAttached({ timeout: 10000 });
// Upload the test file using the proper path from TEST_FILES
const filePath = fileName === 'test1.png' ? TEST_FILES.test1 : `../tests/test_images/${fileName}`;
await fileInput.setInputFiles(filePath);
// Verify file is added to the list by looking for the filename
await expect(this.page.getByText(fileName)).toBeVisible({ timeout: 5000 });
// Look for the "Upload All" button which appears after files are selected
const uploadButton = this.page.locator('button:has-text("Upload All"), button:has-text("Upload")');
if (await uploadButton.isVisible({ timeout: 5000 })) {
// Wait for upload API call
const uploadPromise = this.waitForApiCall('/api/documents', 30000);
await uploadButton.click();
// Wait for upload to complete
await uploadPromise;
console.log('Upload completed successfully');
} else {
console.log('Upload button not found, file may have been uploaded automatically');
}
// Return to documents page
await this.page.goto('/documents');
await this.waitForLoadingToComplete();
console.log('Returned to documents page after upload');
} catch (error) {
console.error('Error uploading test document:', error);
// Return to documents page even if upload failed
await this.page.goto('/documents');
await this.waitForLoadingToComplete();
}
}
async ensureTestDocumentsExist() {
try {
// Give the page time to load before checking for documents
await this.waitForLoadingToComplete();
// Check if there are any documents - use multiple selectors to be safe
const documentSelectors = [
'[data-testid="document-item"]',
'.document-item',
'.document-card',
'.MuiCard-root', // Material-UI cards commonly used for documents
'[role="article"]' // Semantic role for document items
];
let documentCount = 0;
for (const selector of documentSelectors) {
const count = await this.page.locator(selector).count();
if (count > 0) {
documentCount = count;
break;
}
}
console.log(`Found ${documentCount} documents on the page`);
if (documentCount === 0) {
console.log('No documents found, attempting to upload a test document...');
// Upload a test document
await this.uploadTestDocument('test1.png');
}
} catch (error) {
console.log('Error checking for test documents:', error);
// Don't fail the test if document check fails, just log it
}
}
async createTestSource(baseName: string, type: string, options?: {
mockResponse?: boolean;
responseData?: any;
uniqueSuffix?: string;
}) {
// Generate unique source name to avoid conflicts in concurrent tests
const timestamp = Date.now();
const randomSuffix = options?.uniqueSuffix || Math.random().toString(36).substring(7);
const sourceName = `${baseName}_${timestamp}_${randomSuffix}`;
// Set up mock if requested
if (options?.mockResponse) {
const responseData = options.responseData || {
id: `source_${timestamp}`,
name: sourceName,
type: type,
status: 'active',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
};
// Mock the POST request to create source
await this.page.route('**/api/sources', async (route, request) => {
if (request.method() === 'POST') {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(responseData)
});
} else {
await route.continue();
}
});
}
// Click the add source button
await this.page.click('button:has-text("Add Source"), [data-testid="add-source"]');
// Wait for dialog to appear - target the specific dialog paper element
await expect(this.page.getByRole('dialog')).toBeVisible();
// Fill in source details - use more reliable selectors
const nameInput = this.page.getByLabel('Source Name');
await nameInput.fill(sourceName);
// For Material-UI Select, we need to click and then select the option
const typeSelect = this.page.getByLabel('Source Type');
if (await typeSelect.isVisible()) {
await typeSelect.click();
await this.page.getByRole('option', { name: new RegExp(type, 'i') }).click();
}
// Add type-specific fields using label-based selectors
if (type === 'webdav') {
await this.page.getByLabel('Server URL').fill('https://test.webdav.server');
await this.page.getByLabel('Username').fill('testuser');
await this.page.getByLabel('Password').fill('testpass');
} else if (type === 's3') {
await this.page.getByLabel('Bucket Name').fill('test-bucket');
await this.page.getByLabel('Region').fill('us-east-1');
await this.page.getByLabel('Access Key ID').fill('test-access-key');
await this.page.getByLabel('Secret Access Key').fill('test-secret-key');
} else if (type === 'local_folder') {
// For local folder, we need to add a directory path
const addFolderInput = this.page.getByLabel(/Add.*Path/i);
if (await addFolderInput.isVisible()) {
await addFolderInput.fill('/test/path');
await this.page.getByRole('button', { name: /Add.*Folder/i }).click();
}
}
// Submit the form
const createPromise = this.waitForApiCall('/api/sources', 10000);
await this.page.getByRole('button', { name: /Create|Save/i }).click();
// Wait for source to be created
await createPromise;
await this.waitForToast();
// Verify the source appears in the list
await expect(this.page.locator(`[data-testid="source-item"]:has-text("${sourceName}")`)).toBeVisible();
// Return the generated source name so tests can reference it
return sourceName;
}
}