chore: clean .gitignore cache
This commit is contained in:
@@ -1,567 +0,0 @@
|
||||
/**
|
||||
* Backups Page - Creation and List E2E Tests
|
||||
*
|
||||
* Tests for backup creation, listing, deletion, and download functionality.
|
||||
* Covers 17 test scenarios as defined in phase5-implementation.md.
|
||||
*
|
||||
* Test Categories:
|
||||
* - Page Layout (3 tests): heading, create button visibility, role-based access
|
||||
* - Backup List Display (4 tests): empty state, backup list, sorting, loading
|
||||
* - Create Backup Flow (5 tests): create success, toast, list refresh, button disable, error handling
|
||||
* - Delete Backup (3 tests): delete with confirmation, cancel delete, error handling
|
||||
* - Download Backup (2 tests): download trigger, file handling
|
||||
*/
|
||||
|
||||
import { test, expect, loginUser, TEST_PASSWORD } from '../fixtures/auth-fixtures';
|
||||
import { setupBackupsList, BackupFile, BACKUP_SELECTORS } from '../utils/phase5-helpers';
|
||||
import { waitForToast, waitForLoadingComplete, waitForAPIResponse } from '../utils/wait-helpers';
|
||||
|
||||
/**
|
||||
* Mock backup data for testing
|
||||
*/
|
||||
const mockBackups: BackupFile[] = [
|
||||
{ filename: 'backup_2024-01-15_120000.tar.gz', size: 1048576, time: '2024-01-15T12:00:00Z' },
|
||||
{ filename: 'backup_2024-01-14_120000.tar.gz', size: 2097152, time: '2024-01-14T12:00:00Z' },
|
||||
];
|
||||
|
||||
/**
|
||||
* Selectors for the Backups page
|
||||
*/
|
||||
const SELECTORS = {
|
||||
pageTitle: 'h1',
|
||||
createBackupButton: 'button:has-text("Create Backup")',
|
||||
loadingSkeleton: '[data-testid="loading-skeleton"]',
|
||||
emptyState: '[data-testid="empty-state"]',
|
||||
backupTable: '[data-testid="backup-table"]',
|
||||
backupRow: '[data-testid="backup-row"]',
|
||||
downloadBtn: '[data-testid="backup-download-btn"]',
|
||||
restoreBtn: '[data-testid="backup-restore-btn"]',
|
||||
deleteBtn: '[data-testid="backup-delete-btn"]',
|
||||
confirmDialog: '[role="dialog"]',
|
||||
confirmButton: 'button:has-text("Delete")',
|
||||
cancelButton: 'button:has-text("Cancel")',
|
||||
};
|
||||
|
||||
test.describe('Backups Page - Creation and List', () => {
|
||||
// =========================================================================
|
||||
// Page Layout Tests (3 tests)
|
||||
// =========================================================================
|
||||
test.describe('Page Layout', () => {
|
||||
test('should display backups page with correct heading', async ({ page, authenticatedUser }) => {
|
||||
await loginUser(page, authenticatedUser);
|
||||
await page.goto('/tasks/backups');
|
||||
await waitForLoadingComplete(page);
|
||||
|
||||
await expect(page.locator(SELECTORS.pageTitle)).toContainText(/backups/i);
|
||||
});
|
||||
|
||||
test('should show Create Backup button for admin users', async ({ page, adminUser }) => {
|
||||
await loginUser(page, adminUser);
|
||||
await page.goto('/tasks/backups');
|
||||
await waitForLoadingComplete(page);
|
||||
|
||||
const createButton = page.locator(SELECTORS.createBackupButton).first();
|
||||
await expect(createButton).toBeVisible();
|
||||
await expect(createButton).toBeEnabled();
|
||||
});
|
||||
|
||||
test('should hide Create Backup button for guest users', async ({ page, guestUser }) => {
|
||||
await loginUser(page, guestUser);
|
||||
await page.goto('/tasks/backups');
|
||||
await waitForLoadingComplete(page);
|
||||
|
||||
// Guest users should not see any Create Backup button
|
||||
const createButton = page.locator(SELECTORS.createBackupButton);
|
||||
await expect(createButton).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// Backup List Display Tests (4 tests)
|
||||
// =========================================================================
|
||||
test.describe('Backup List Display', () => {
|
||||
test('should display empty state when no backups exist', async ({ page, authenticatedUser }) => {
|
||||
await loginUser(page, authenticatedUser);
|
||||
|
||||
// Mock empty response
|
||||
await page.route('**/api/v1/backups', async (route) => {
|
||||
if (route.request().method() === 'GET') {
|
||||
await route.fulfill({ status: 200, json: [] });
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto('/tasks/backups');
|
||||
await waitForLoadingComplete(page);
|
||||
|
||||
const emptyState = page.locator(SELECTORS.emptyState);
|
||||
await expect(emptyState).toBeVisible();
|
||||
});
|
||||
|
||||
test('should display list of existing backups', async ({ page, authenticatedUser }) => {
|
||||
await loginUser(page, authenticatedUser);
|
||||
|
||||
// Mock backup list response
|
||||
await page.route('**/api/v1/backups', async (route) => {
|
||||
if (route.request().method() === 'GET') {
|
||||
await route.fulfill({ status: 200, json: mockBackups });
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto('/tasks/backups');
|
||||
await waitForLoadingComplete(page);
|
||||
|
||||
// Verify both backups are displayed
|
||||
await expect(page.getByText('backup_2024-01-15_120000.tar.gz')).toBeVisible();
|
||||
await expect(page.getByText('backup_2024-01-14_120000.tar.gz')).toBeVisible();
|
||||
|
||||
// Verify size is displayed (formatted)
|
||||
await expect(page.getByText('1.00 MB')).toBeVisible();
|
||||
await expect(page.getByText('2.00 MB')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should sort backups by date newest first', async ({ page, authenticatedUser }) => {
|
||||
await loginUser(page, authenticatedUser);
|
||||
|
||||
// Mock backup list with specific order
|
||||
const sortedBackups: BackupFile[] = [
|
||||
{ filename: 'backup_2024-01-16_120000.tar.gz', size: 512000, time: '2024-01-16T12:00:00Z' },
|
||||
{ filename: 'backup_2024-01-15_120000.tar.gz', size: 1048576, time: '2024-01-15T12:00:00Z' },
|
||||
{ filename: 'backup_2024-01-14_120000.tar.gz', size: 2097152, time: '2024-01-14T12:00:00Z' },
|
||||
];
|
||||
|
||||
await page.route('**/api/v1/backups', async (route) => {
|
||||
if (route.request().method() === 'GET') {
|
||||
await route.fulfill({ status: 200, json: sortedBackups });
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto('/tasks/backups');
|
||||
await waitForLoadingComplete(page);
|
||||
|
||||
// Get all backup filenames in order
|
||||
const rows = page.locator('table tbody tr, [role="row"]').filter({ hasNot: page.locator('th') });
|
||||
const firstRow = rows.first();
|
||||
const lastRow = rows.last();
|
||||
|
||||
// Newest backup should appear first
|
||||
await expect(firstRow).toContainText('backup_2024-01-16');
|
||||
await expect(lastRow).toContainText('backup_2024-01-14');
|
||||
});
|
||||
|
||||
test('should show loading skeleton while fetching', async ({ page, authenticatedUser }) => {
|
||||
await loginUser(page, authenticatedUser);
|
||||
|
||||
// Delay the response to observe loading state
|
||||
await page.route('**/api/v1/backups', async (route) => {
|
||||
if (route.request().method() === 'GET') {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
await route.fulfill({ status: 200, json: mockBackups });
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto('/tasks/backups');
|
||||
|
||||
// Should show loading skeleton initially
|
||||
const skeleton = page.locator(SELECTORS.loadingSkeleton);
|
||||
await expect(skeleton).toBeVisible({ timeout: 2000 });
|
||||
|
||||
// After loading completes, skeleton should disappear
|
||||
await waitForLoadingComplete(page, { timeout: 5000 });
|
||||
await expect(skeleton).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// Create Backup Flow Tests (5 tests)
|
||||
// =========================================================================
|
||||
test.describe('Create Backup Flow', () => {
|
||||
test('should create a new backup successfully', async ({ page, adminUser }) => {
|
||||
await loginUser(page, adminUser);
|
||||
|
||||
const newBackup: BackupFile = {
|
||||
filename: 'backup_2024-01-16_120000.tar.gz',
|
||||
size: 512000,
|
||||
time: new Date().toISOString(),
|
||||
};
|
||||
|
||||
let postCalled = false;
|
||||
|
||||
await page.route('**/api/v1/backups', async (route) => {
|
||||
if (route.request().method() === 'POST') {
|
||||
postCalled = true;
|
||||
await route.fulfill({ status: 201, json: newBackup });
|
||||
} else if (route.request().method() === 'GET') {
|
||||
await route.fulfill({ status: 200, json: mockBackups });
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto('/tasks/backups');
|
||||
await waitForLoadingComplete(page);
|
||||
|
||||
// Click create backup button and wait for API response concurrently
|
||||
await Promise.all([
|
||||
page.waitForResponse(r => r.url().includes('/api/v1/backups') && r.request().method() === 'POST' && r.status() === 201),
|
||||
page.click(SELECTORS.createBackupButton),
|
||||
]);
|
||||
|
||||
// Verify POST was called
|
||||
expect(postCalled).toBe(true);
|
||||
});
|
||||
|
||||
test('should show success toast after backup creation', async ({ page, adminUser }) => {
|
||||
await loginUser(page, adminUser);
|
||||
|
||||
const newBackup: BackupFile = {
|
||||
filename: 'backup_2024-01-16_120000.tar.gz',
|
||||
size: 512000,
|
||||
time: new Date().toISOString(),
|
||||
};
|
||||
|
||||
await page.route('**/api/v1/backups', async (route) => {
|
||||
if (route.request().method() === 'POST') {
|
||||
await route.fulfill({ status: 201, json: newBackup });
|
||||
} else if (route.request().method() === 'GET') {
|
||||
await route.fulfill({ status: 200, json: mockBackups });
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto('/tasks/backups');
|
||||
await waitForLoadingComplete(page);
|
||||
|
||||
// Click create backup button
|
||||
await page.click(SELECTORS.createBackupButton);
|
||||
|
||||
// Wait for success toast
|
||||
await waitForToast(page, /success|created/i, { type: 'success' });
|
||||
});
|
||||
|
||||
test('should update backup list with new backup', async ({ page, adminUser }) => {
|
||||
await loginUser(page, adminUser);
|
||||
|
||||
const newBackup: BackupFile = {
|
||||
filename: 'backup_2024-01-16_120000.tar.gz',
|
||||
size: 512000,
|
||||
time: new Date().toISOString(),
|
||||
};
|
||||
|
||||
let requestCount = 0;
|
||||
|
||||
await page.route('**/api/v1/backups', async (route) => {
|
||||
if (route.request().method() === 'POST') {
|
||||
await route.fulfill({ status: 201, json: newBackup });
|
||||
} else if (route.request().method() === 'GET') {
|
||||
requestCount++;
|
||||
// Return updated list after creation
|
||||
const backups = requestCount > 1 ? [newBackup, ...mockBackups] : mockBackups;
|
||||
await route.fulfill({ status: 200, json: backups });
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto('/tasks/backups');
|
||||
await waitForLoadingComplete(page);
|
||||
|
||||
// Initial state - should not show new backup
|
||||
await expect(page.getByText('backup_2024-01-16_120000.tar.gz')).not.toBeVisible();
|
||||
|
||||
// Click create backup button
|
||||
await page.click(SELECTORS.createBackupButton);
|
||||
|
||||
// Wait for success toast (which indicates the backup was created)
|
||||
await waitForToast(page, /success|created/i, { type: 'success' });
|
||||
|
||||
// New backup should now be visible after list refresh
|
||||
await expect(page.getByText('backup_2024-01-16_120000.tar.gz')).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
test('should disable create button while in progress', async ({ page, adminUser }) => {
|
||||
await loginUser(page, adminUser);
|
||||
|
||||
await page.route('**/api/v1/backups', async (route) => {
|
||||
if (route.request().method() === 'POST') {
|
||||
// Delay response to observe disabled state
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
await route.fulfill({
|
||||
status: 201,
|
||||
json: { filename: 'test.tar.gz', size: 100, time: new Date().toISOString() },
|
||||
});
|
||||
} else if (route.request().method() === 'GET') {
|
||||
await route.fulfill({ status: 200, json: mockBackups });
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto('/tasks/backups');
|
||||
await waitForLoadingComplete(page);
|
||||
|
||||
const createButton = page.locator(SELECTORS.createBackupButton);
|
||||
|
||||
// Click create button
|
||||
await createButton.click();
|
||||
|
||||
// Button should be disabled during request
|
||||
await expect(createButton).toBeDisabled();
|
||||
|
||||
// Wait for API response
|
||||
await waitForAPIResponse(page, '/api/v1/backups', { status: 201 });
|
||||
|
||||
// After completion, button should be enabled again
|
||||
await expect(createButton).toBeEnabled({ timeout: 5000 });
|
||||
});
|
||||
|
||||
test('should handle backup creation failure', async ({ page, adminUser }) => {
|
||||
await loginUser(page, adminUser);
|
||||
|
||||
await page.route('**/api/v1/backups', async (route) => {
|
||||
if (route.request().method() === 'POST') {
|
||||
await route.fulfill({
|
||||
status: 500,
|
||||
json: { error: 'Internal server error' },
|
||||
});
|
||||
} else if (route.request().method() === 'GET') {
|
||||
await route.fulfill({ status: 200, json: mockBackups });
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto('/tasks/backups');
|
||||
await waitForLoadingComplete(page);
|
||||
|
||||
// Click create backup button
|
||||
await page.click(SELECTORS.createBackupButton);
|
||||
|
||||
// Wait for error toast
|
||||
await waitForToast(page, /error|failed/i, { type: 'error' });
|
||||
});
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// Delete Backup Tests (3 tests)
|
||||
// =========================================================================
|
||||
test.describe('Delete Backup', () => {
|
||||
test('should show confirmation dialog before deleting', async ({ page, adminUser }) => {
|
||||
await loginUser(page, adminUser);
|
||||
|
||||
await page.route('**/api/v1/backups', async (route) => {
|
||||
if (route.request().method() === 'GET') {
|
||||
await route.fulfill({ status: 200, json: mockBackups });
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto('/tasks/backups');
|
||||
await waitForLoadingComplete(page);
|
||||
|
||||
// Click delete on first backup
|
||||
const deleteButton = page.locator(SELECTORS.deleteBtn).first();
|
||||
await deleteButton.click();
|
||||
|
||||
// Verify dialog appears
|
||||
const dialog = page.locator(SELECTORS.confirmDialog);
|
||||
await expect(dialog).toBeVisible();
|
||||
|
||||
// Verify dialog contains confirmation text
|
||||
await expect(dialog).toContainText(/delete|confirm/i);
|
||||
});
|
||||
|
||||
test('should delete backup after confirmation', async ({ page, adminUser }) => {
|
||||
await loginUser(page, adminUser);
|
||||
|
||||
const filename = 'backup_2024-01-15_120000.tar.gz';
|
||||
let deleteRequested = false;
|
||||
|
||||
await page.route('**/api/v1/backups', async (route) => {
|
||||
if (route.request().method() === 'GET') {
|
||||
await route.fulfill({ status: 200, json: mockBackups });
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
await page.route(`**/api/v1/backups/${filename}`, async (route) => {
|
||||
if (route.request().method() === 'DELETE') {
|
||||
deleteRequested = true;
|
||||
await route.fulfill({ status: 204 });
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto('/tasks/backups');
|
||||
await waitForLoadingComplete(page);
|
||||
|
||||
// Click delete on first backup
|
||||
const deleteButton = page.locator(SELECTORS.deleteBtn).first();
|
||||
await deleteButton.click();
|
||||
|
||||
// Wait for dialog
|
||||
const dialog = page.locator(SELECTORS.confirmDialog);
|
||||
await expect(dialog).toBeVisible();
|
||||
|
||||
// Click confirm button and wait for DELETE request concurrently
|
||||
const confirmButton = dialog.locator(SELECTORS.confirmButton);
|
||||
await Promise.all([
|
||||
page.waitForResponse(r => r.url().includes(`/api/v1/backups/${filename}`) && r.request().method() === 'DELETE' && r.status() === 204),
|
||||
confirmButton.click(),
|
||||
]);
|
||||
|
||||
// Verify DELETE was called
|
||||
expect(deleteRequested).toBe(true);
|
||||
|
||||
// Dialog should close
|
||||
await expect(dialog).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('should cancel delete when clicking cancel button', async ({ page, adminUser }) => {
|
||||
await loginUser(page, adminUser);
|
||||
|
||||
let deleteRequested = false;
|
||||
|
||||
await page.route('**/api/v1/backups', async (route) => {
|
||||
if (route.request().method() === 'GET') {
|
||||
await route.fulfill({ status: 200, json: mockBackups });
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
await page.route('**/api/v1/backups/*', async (route) => {
|
||||
if (route.request().method() === 'DELETE') {
|
||||
deleteRequested = true;
|
||||
await route.fulfill({ status: 204 });
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto('/tasks/backups');
|
||||
await waitForLoadingComplete(page);
|
||||
|
||||
// Click delete on first backup
|
||||
const deleteButton = page.locator(SELECTORS.deleteBtn).first();
|
||||
await deleteButton.click();
|
||||
|
||||
// Wait for dialog
|
||||
const dialog = page.locator(SELECTORS.confirmDialog);
|
||||
await expect(dialog).toBeVisible();
|
||||
|
||||
// Click cancel button
|
||||
const cancelButton = dialog.locator(SELECTORS.cancelButton);
|
||||
await cancelButton.click();
|
||||
|
||||
// Dialog should close
|
||||
await expect(dialog).not.toBeVisible();
|
||||
|
||||
// DELETE should not have been called
|
||||
expect(deleteRequested).toBe(false);
|
||||
|
||||
// Backup should still be visible
|
||||
await expect(page.getByText('backup_2024-01-15_120000.tar.gz')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// Download Backup Tests (2 tests)
|
||||
// =========================================================================
|
||||
test.describe('Download Backup', () => {
|
||||
test('should download backup file successfully', async ({ page, adminUser }) => {
|
||||
await loginUser(page, adminUser);
|
||||
|
||||
const filename = 'backup_2024-01-15_120000.tar.gz';
|
||||
|
||||
await page.route('**/api/v1/backups', async (route) => {
|
||||
if (route.request().method() === 'GET') {
|
||||
await route.fulfill({ status: 200, json: mockBackups });
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
// Mock download endpoint
|
||||
await page.route(`**/api/v1/backups/${filename}/download`, async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/gzip',
|
||||
'Content-Disposition': `attachment; filename="${filename}"`,
|
||||
},
|
||||
body: Buffer.from('mock backup content'),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto('/tasks/backups');
|
||||
await waitForLoadingComplete(page);
|
||||
|
||||
// Track download event - The component uses window.location.href for downloads
|
||||
// Since Playwright can't track navigation-based downloads directly,
|
||||
// we verify the download button triggers the correct action
|
||||
const downloadButton = page.locator(SELECTORS.downloadBtn).first();
|
||||
await expect(downloadButton).toBeVisible();
|
||||
await expect(downloadButton).toBeEnabled();
|
||||
|
||||
// For actual download verification in a real scenario, we'd use:
|
||||
// const downloadPromise = page.waitForEvent('download');
|
||||
// await downloadButton.click();
|
||||
// const download = await downloadPromise;
|
||||
// expect(download.suggestedFilename()).toBe(filename);
|
||||
|
||||
// For this test, we verify the button is clickable and properly rendered
|
||||
const buttonTitle = await downloadButton.getAttribute('title');
|
||||
expect(buttonTitle).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should show error toast when download fails', async ({ page, adminUser }) => {
|
||||
await loginUser(page, adminUser);
|
||||
|
||||
const filename = 'backup_2024-01-15_120000.tar.gz';
|
||||
|
||||
await page.route('**/api/v1/backups', async (route) => {
|
||||
if (route.request().method() === 'GET') {
|
||||
await route.fulfill({ status: 200, json: mockBackups });
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
// Mock download endpoint with failure
|
||||
await page.route(`**/api/v1/backups/${filename}/download`, async (route) => {
|
||||
await route.fulfill({
|
||||
status: 404,
|
||||
json: { error: 'Backup file not found' },
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto('/tasks/backups');
|
||||
await waitForLoadingComplete(page);
|
||||
|
||||
// The download button uses window.location.href which navigates away
|
||||
// For error handling tests, we verify the download button is present
|
||||
// In the actual component, download errors would be handled differently
|
||||
// since window.location.href navigation can't be caught by JavaScript
|
||||
|
||||
const downloadButton = page.locator(SELECTORS.downloadBtn).first();
|
||||
await expect(downloadButton).toBeVisible();
|
||||
|
||||
// Note: The Backups.tsx component uses window.location.href for downloads,
|
||||
// which means download errors result in browser navigation to an error page
|
||||
// rather than a toast notification. This is a known limitation of the current
|
||||
// implementation. A better approach would use fetch() with blob download.
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user