411 lines
No EOL
14 KiB
TypeScript
411 lines
No EOL
14 KiB
TypeScript
import { test, expect, Page } from '@playwright/test';
|
|
import * as path from 'path';
|
|
|
|
// Test data
|
|
const testEmail = `user+files-${Date.now()}@lab.veza`;
|
|
const testPassword = `V3za!files-${Date.now()}`;
|
|
|
|
// Helper to register and login
|
|
async function registerAndLogin(page: Page) {
|
|
await page.goto('/register');
|
|
await page.fill('input[type="email"]', testEmail);
|
|
await page.fill('input[type="password"]', testPassword);
|
|
|
|
const confirmField = page.locator('input[name="confirmPassword"]');
|
|
if (await confirmField.isVisible()) {
|
|
await confirmField.fill(testPassword);
|
|
}
|
|
|
|
await page.locator('button[type="submit"]').click();
|
|
await expect(page).toHaveURL(/\/(dashboard|home|app|files)/, { timeout: 10000 });
|
|
}
|
|
|
|
test.describe('File Management', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await registerAndLogin(page);
|
|
});
|
|
|
|
test('should load files interface', async ({ page }) => {
|
|
await page.goto('/files');
|
|
|
|
// Check main elements
|
|
await expect(page.locator('[data-testid="files-container"], .files-container, #files')).toBeVisible();
|
|
await expect(page.locator('button:has-text("Upload"), [data-testid="upload-button"]')).toBeVisible();
|
|
|
|
// Check for file list/grid
|
|
const filesList = page.locator('[data-testid="files-list"], .files-list, .files-grid');
|
|
await expect(filesList).toBeVisible();
|
|
});
|
|
|
|
test('should upload text file', async ({ page }) => {
|
|
await page.goto('/files');
|
|
|
|
// Create test file
|
|
const fileName = `test-${Date.now()}.txt`;
|
|
const fileContent = 'This is a test file content';
|
|
|
|
// Find upload input
|
|
const uploadInput = page.locator('input[type="file"]');
|
|
|
|
if (!await uploadInput.isVisible()) {
|
|
// Click upload button to reveal input
|
|
await page.locator('button:has-text("Upload")').click();
|
|
}
|
|
|
|
// Upload file
|
|
await uploadInput.setInputFiles({
|
|
name: fileName,
|
|
mimeType: 'text/plain',
|
|
buffer: Buffer.from(fileContent),
|
|
});
|
|
|
|
// Wait for upload to complete
|
|
await expect(page.locator(`text="${fileName}"`)).toBeVisible({ timeout: 10000 });
|
|
|
|
// Check file details
|
|
const fileItem = page.locator(`[data-filename="${fileName}"], text="${fileName}"`).locator('xpath=ancestor::*[contains(@class, "file")]');
|
|
|
|
// Should show file size
|
|
await expect(fileItem.locator('text=/\\d+\\s*(B|KB|MB)/i')).toBeVisible();
|
|
});
|
|
|
|
test('should upload image file', async ({ page }) => {
|
|
await page.goto('/files');
|
|
|
|
const imagePath = path.join(__dirname, '../../data/images/avatar.jpg');
|
|
|
|
// Upload image
|
|
const uploadInput = page.locator('input[type="file"]');
|
|
await uploadInput.setInputFiles(imagePath);
|
|
|
|
// Wait for upload
|
|
await expect(page.locator('text=avatar.jpg')).toBeVisible({ timeout: 10000 });
|
|
|
|
// Should show thumbnail
|
|
const thumbnail = page.locator('img[src*="avatar"], [data-testid="file-thumbnail"]');
|
|
if (await thumbnail.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
const src = await thumbnail.getAttribute('src');
|
|
expect(src).toBeTruthy();
|
|
}
|
|
});
|
|
|
|
test('should handle multiple file upload', async ({ page }) => {
|
|
await page.goto('/files');
|
|
|
|
// Create multiple test files
|
|
const files = [];
|
|
for (let i = 0; i < 3; i++) {
|
|
files.push({
|
|
name: `multi-${i}-${Date.now()}.txt`,
|
|
mimeType: 'text/plain',
|
|
buffer: Buffer.from(`Content of file ${i}`),
|
|
});
|
|
}
|
|
|
|
// Upload multiple files
|
|
const uploadInput = page.locator('input[type="file"]');
|
|
await uploadInput.setInputFiles(files);
|
|
|
|
// All files should appear
|
|
for (const file of files) {
|
|
await expect(page.locator(`text="${file.name}"`)).toBeVisible({ timeout: 10000 });
|
|
}
|
|
});
|
|
|
|
test('should show upload progress', async ({ page }) => {
|
|
await page.goto('/files');
|
|
|
|
// Create large file (1MB)
|
|
const largeContent = 'x'.repeat(1024 * 1024);
|
|
const fileName = `large-${Date.now()}.txt`;
|
|
|
|
// Start upload
|
|
const uploadInput = page.locator('input[type="file"]');
|
|
await uploadInput.setInputFiles({
|
|
name: fileName,
|
|
mimeType: 'text/plain',
|
|
buffer: Buffer.from(largeContent),
|
|
});
|
|
|
|
// Look for progress indicator
|
|
const progressBar = page.locator('[role="progressbar"], .upload-progress, [data-testid="upload-progress"]');
|
|
|
|
if (await progressBar.isVisible({ timeout: 1000 }).catch(() => false)) {
|
|
// Progress should be visible during upload
|
|
const progress = await progressBar.getAttribute('aria-valuenow') ||
|
|
await progressBar.getAttribute('value');
|
|
expect(progress).toBeTruthy();
|
|
}
|
|
});
|
|
|
|
test('should handle file size limits', async ({ page }) => {
|
|
await page.goto('/files');
|
|
|
|
// Try to upload file larger than limit (simulate)
|
|
await page.evaluate(() => {
|
|
// Trigger file size error
|
|
const event = new CustomEvent('error', {
|
|
detail: { message: 'File too large' }
|
|
});
|
|
window.dispatchEvent(event);
|
|
});
|
|
|
|
// Or try actual large file
|
|
const veryLargeContent = 'x'.repeat(50 * 1024 * 1024); // 50MB
|
|
const uploadInput = page.locator('input[type="file"]');
|
|
|
|
try {
|
|
await uploadInput.setInputFiles({
|
|
name: 'too-large.txt',
|
|
mimeType: 'text/plain',
|
|
buffer: Buffer.from(veryLargeContent),
|
|
});
|
|
|
|
// Should show error
|
|
await expect(page.locator('text=/too large|size limit|exceeds/i')).toBeVisible({ timeout: 5000 });
|
|
} catch (e) {
|
|
// File might be too large for Playwright itself
|
|
console.log('Skipping very large file test');
|
|
}
|
|
});
|
|
|
|
test('should preview text files', async ({ page }) => {
|
|
await page.goto('/files');
|
|
|
|
// Upload text file
|
|
const fileName = `preview-${Date.now()}.txt`;
|
|
const fileContent = 'Preview this text content\nLine 2\nLine 3';
|
|
|
|
await page.locator('input[type="file"]').setInputFiles({
|
|
name: fileName,
|
|
mimeType: 'text/plain',
|
|
buffer: Buffer.from(fileContent),
|
|
});
|
|
|
|
// Wait for file to appear
|
|
await page.waitForTimeout(2000);
|
|
|
|
// Click file to preview
|
|
await page.locator(`text="${fileName}"`).click();
|
|
|
|
// Should show preview
|
|
const preview = page.locator('[data-testid="file-preview"], .file-preview, .modal');
|
|
if (await preview.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
await expect(preview.locator('text="Preview this text content"')).toBeVisible();
|
|
}
|
|
});
|
|
|
|
test('should download files', async ({ page }) => {
|
|
await page.goto('/files');
|
|
|
|
// Upload a file first
|
|
const fileName = `download-${Date.now()}.txt`;
|
|
await page.locator('input[type="file"]').setInputFiles({
|
|
name: fileName,
|
|
mimeType: 'text/plain',
|
|
buffer: Buffer.from('Download me'),
|
|
});
|
|
|
|
await page.waitForTimeout(2000);
|
|
|
|
// Find download button
|
|
const fileItem = page.locator(`text="${fileName}"`).locator('xpath=ancestor::*[contains(@class, "file")]');
|
|
const downloadButton = fileItem.locator('button[aria-label="Download"], [data-testid="download-file"]');
|
|
|
|
if (await downloadButton.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
// Start download
|
|
const downloadPromise = page.waitForEvent('download');
|
|
await downloadButton.click();
|
|
const download = await downloadPromise;
|
|
|
|
// Verify download
|
|
expect(download.suggestedFilename()).toBe(fileName);
|
|
}
|
|
});
|
|
|
|
test('should delete files', async ({ page }) => {
|
|
await page.goto('/files');
|
|
|
|
// Upload a file
|
|
const fileName = `delete-${Date.now()}.txt`;
|
|
await page.locator('input[type="file"]').setInputFiles({
|
|
name: fileName,
|
|
mimeType: 'text/plain',
|
|
buffer: Buffer.from('Delete me'),
|
|
});
|
|
|
|
await page.waitForTimeout(2000);
|
|
|
|
// Find delete button
|
|
const fileItem = page.locator(`text="${fileName}"`).locator('xpath=ancestor::*[contains(@class, "file")]');
|
|
const deleteButton = fileItem.locator('button[aria-label="Delete"], [data-testid="delete-file"]');
|
|
|
|
if (await deleteButton.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
await deleteButton.click();
|
|
|
|
// Confirm deletion if needed
|
|
const confirmButton = page.locator('button:has-text("Confirm"), button:has-text("Delete")').last();
|
|
if (await confirmButton.isVisible({ timeout: 1000 }).catch(() => false)) {
|
|
await confirmButton.click();
|
|
}
|
|
|
|
// File should be removed
|
|
await expect(page.locator(`text="${fileName}"`)).not.toBeVisible({ timeout: 5000 });
|
|
}
|
|
});
|
|
|
|
test('should rename files', async ({ page }) => {
|
|
await page.goto('/files');
|
|
|
|
// Upload a file
|
|
const fileName = `rename-${Date.now()}.txt`;
|
|
await page.locator('input[type="file"]').setInputFiles({
|
|
name: fileName,
|
|
mimeType: 'text/plain',
|
|
buffer: Buffer.from('Rename me'),
|
|
});
|
|
|
|
await page.waitForTimeout(2000);
|
|
|
|
// Find rename option
|
|
const fileItem = page.locator(`text="${fileName}"`).locator('xpath=ancestor::*[contains(@class, "file")]');
|
|
|
|
// Right-click or find menu
|
|
await fileItem.click({ button: 'right' });
|
|
const renameOption = page.locator('text="Rename"');
|
|
|
|
if (await renameOption.isVisible({ timeout: 1000 }).catch(() => false)) {
|
|
await renameOption.click();
|
|
|
|
// Enter new name
|
|
const newName = `renamed-${Date.now()}.txt`;
|
|
const input = page.locator('input[value*="rename"]');
|
|
await input.fill(newName);
|
|
await input.press('Enter');
|
|
|
|
// New name should appear
|
|
await expect(page.locator(`text="${newName}"`)).toBeVisible({ timeout: 5000 });
|
|
await expect(page.locator(`text="${fileName}"`)).not.toBeVisible();
|
|
}
|
|
});
|
|
|
|
test('should search files', async ({ page }) => {
|
|
await page.goto('/files');
|
|
|
|
// Upload files with different names
|
|
const searchTerm = `searchable-${Date.now()}`;
|
|
const files = [
|
|
`${searchTerm}-1.txt`,
|
|
`${searchTerm}-2.txt`,
|
|
`other-file.txt`,
|
|
];
|
|
|
|
for (const fileName of files) {
|
|
await page.locator('input[type="file"]').setInputFiles({
|
|
name: fileName,
|
|
mimeType: 'text/plain',
|
|
buffer: Buffer.from('Content'),
|
|
});
|
|
await page.waitForTimeout(500);
|
|
}
|
|
|
|
// Search
|
|
const searchInput = page.locator('input[placeholder*="Search"], [data-testid="search-files"]');
|
|
if (await searchInput.isVisible()) {
|
|
await searchInput.fill(searchTerm);
|
|
await searchInput.press('Enter');
|
|
|
|
// Should show only matching files
|
|
await expect(page.locator(`text="${searchTerm}-1.txt"`)).toBeVisible();
|
|
await expect(page.locator(`text="${searchTerm}-2.txt"`)).toBeVisible();
|
|
await expect(page.locator('text="other-file.txt"')).not.toBeVisible();
|
|
}
|
|
});
|
|
|
|
test('should sort files', async ({ page }) => {
|
|
await page.goto('/files');
|
|
|
|
// Upload files with different dates/sizes
|
|
const files = [
|
|
{ name: 'a-file.txt', content: 'Small' },
|
|
{ name: 'z-file.txt', content: 'Large content here' },
|
|
{ name: 'm-file.txt', content: 'Medium size' },
|
|
];
|
|
|
|
for (const file of files) {
|
|
await page.locator('input[type="file"]').setInputFiles({
|
|
name: file.name,
|
|
mimeType: 'text/plain',
|
|
buffer: Buffer.from(file.content),
|
|
});
|
|
await page.waitForTimeout(500);
|
|
}
|
|
|
|
// Find sort dropdown
|
|
const sortDropdown = page.locator('select[aria-label*="Sort"], [data-testid="sort-files"]');
|
|
|
|
if (await sortDropdown.isVisible()) {
|
|
// Sort by name
|
|
await sortDropdown.selectOption('name');
|
|
|
|
// Check order
|
|
const fileNames = await page.locator('.file-name').allTextContents();
|
|
expect(fileNames[0]).toContain('a-file');
|
|
expect(fileNames[fileNames.length - 1]).toContain('z-file');
|
|
}
|
|
});
|
|
|
|
test('should show file details', async ({ page }) => {
|
|
await page.goto('/files');
|
|
|
|
// Upload file
|
|
const fileName = `details-${Date.now()}.txt`;
|
|
const fileContent = 'File with details';
|
|
|
|
await page.locator('input[type="file"]').setInputFiles({
|
|
name: fileName,
|
|
mimeType: 'text/plain',
|
|
buffer: Buffer.from(fileContent),
|
|
});
|
|
|
|
await page.waitForTimeout(2000);
|
|
|
|
// Click info button or file
|
|
const fileItem = page.locator(`text="${fileName}"`).locator('xpath=ancestor::*[contains(@class, "file")]');
|
|
const infoButton = fileItem.locator('button[aria-label="Info"], [data-testid="file-info"]');
|
|
|
|
if (await infoButton.isVisible()) {
|
|
await infoButton.click();
|
|
} else {
|
|
await fileItem.click();
|
|
}
|
|
|
|
// Should show details modal/panel
|
|
const details = page.locator('[data-testid="file-details"], .file-details');
|
|
if (await details.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
await expect(details.locator(`text="${fileName}"`)).toBeVisible();
|
|
await expect(details.locator('text=/size/i')).toBeVisible();
|
|
await expect(details.locator('text=/type.*text/i')).toBeVisible();
|
|
await expect(details.locator('text=/uploaded/i')).toBeVisible();
|
|
}
|
|
});
|
|
|
|
test('should handle drag and drop upload', async ({ page }) => {
|
|
await page.goto('/files');
|
|
|
|
// Find drop zone
|
|
const dropZone = page.locator('[data-testid="drop-zone"], .drop-zone, .files-container');
|
|
|
|
// Create data transfer
|
|
const dataTransfer = await page.evaluateHandle(() => new DataTransfer());
|
|
|
|
// Simulate drag over
|
|
await dropZone.dispatchEvent('dragover', { dataTransfer });
|
|
|
|
// Should show drop indicator
|
|
await expect(dropZone).toHaveClass(/drag-over|dropping/);
|
|
|
|
// Note: Full drag-drop file upload is complex in Playwright
|
|
// This test mainly checks the UI responds to drag events
|
|
});
|
|
}); |