Some checks are pending
Go Benchmark / Performance Regression Check (push) Waiting to run
Cerberus Integration / Cerberus Security Stack Integration (push) Waiting to run
Upload Coverage to Codecov / Backend Codecov Upload (push) Waiting to run
Upload Coverage to Codecov / Frontend Codecov Upload (push) Waiting to run
CodeQL - Analyze / CodeQL analysis (go) (push) Waiting to run
CodeQL - Analyze / CodeQL analysis (javascript-typescript) (push) Waiting to run
CrowdSec Integration / CrowdSec Bouncer Integration (push) Waiting to run
Docker Build, Publish & Test / build-and-push (push) Waiting to run
Docker Build, Publish & Test / Security Scan PR Image (push) Blocked by required conditions
Quality Checks / Auth Route Protection Contract (push) Waiting to run
Quality Checks / Codecov Trigger/Comment Parity Guard (push) Waiting to run
Quality Checks / Backend (Go) (push) Waiting to run
Quality Checks / Frontend (React) (push) Waiting to run
Rate Limit integration / Rate Limiting Integration (push) Waiting to run
Security Scan (PR) / Trivy Binary Scan (push) Waiting to run
Supply Chain Verification (PR) / Verify Supply Chain (push) Waiting to run
WAF integration / Coraza WAF Integration (push) Waiting to run
710 lines
24 KiB
TypeScript
Executable File
710 lines
24 KiB
TypeScript
Executable File
/**
|
|
* Logs Page - Static Log File Viewing E2E Tests
|
|
*
|
|
* Tests for log file listing, content display, filtering, pagination, and download.
|
|
* Covers 12 test scenarios optimized for WebKit and cross-browser compatibility.
|
|
*
|
|
* Test Categories:
|
|
* - Page Layout (3 tests): heading, file list, filter section
|
|
* - Log File List (2 tests): display files with metadata, select file
|
|
* - Log Content Display (2 tests): show columns, highlight error entries
|
|
* - Pagination (3 tests): navigate pages, page info, button states
|
|
* - Search/Filter (2 tests): text search, level filter
|
|
* - Download (2 tests): download file, error handling
|
|
*
|
|
* Route: /tasks/logs
|
|
* Component: Logs.tsx
|
|
* Updated: 2024-02-10 for full WebKit support
|
|
*/
|
|
|
|
import { test, expect, loginUser, TEST_PASSWORD } from '../fixtures/auth-fixtures';
|
|
import { waitForLoadingComplete, waitForAPIResponse } from '../utils/wait-helpers';
|
|
import type { Page } from '@playwright/test';
|
|
|
|
/**
|
|
* Mock log files for testing
|
|
*/
|
|
const mockLogFiles = [
|
|
{ name: 'access.log', size: 1048576, modified: '2024-01-15T12:00:00Z' },
|
|
{ name: 'error.log', size: 256000, modified: '2024-01-15T11:30:00Z' },
|
|
{ name: 'caddy.log', size: 512000, modified: '2024-01-14T10:00:00Z' },
|
|
];
|
|
|
|
/**
|
|
* Mock log entries for content display testing
|
|
*/
|
|
const mockLogEntries = [
|
|
{
|
|
level: 'info',
|
|
ts: Math.floor(Date.now() / 1000),
|
|
logger: 'http.log.access',
|
|
msg: 'handled request',
|
|
request: {
|
|
remote_ip: '192.168.1.100',
|
|
method: 'GET',
|
|
host: 'api.example.com',
|
|
uri: '/api/v1/users',
|
|
proto: 'HTTP/2',
|
|
},
|
|
status: 200,
|
|
duration: 0.045,
|
|
size: 1234,
|
|
},
|
|
{
|
|
level: 'error',
|
|
ts: Math.floor(Date.now() / 1000) - 60,
|
|
logger: 'http.log.access',
|
|
msg: 'connection refused',
|
|
request: {
|
|
remote_ip: '192.168.1.101',
|
|
method: 'POST',
|
|
host: 'api.example.com',
|
|
uri: '/api/v1/auth/login',
|
|
proto: 'HTTP/2',
|
|
},
|
|
status: 502,
|
|
duration: 5.023,
|
|
size: 0,
|
|
},
|
|
{
|
|
level: 'warn',
|
|
ts: Math.floor(Date.now() / 1000) - 120,
|
|
logger: 'http.log.access',
|
|
msg: 'rate limit exceeded',
|
|
request: {
|
|
remote_ip: '10.0.0.50',
|
|
method: 'GET',
|
|
host: 'web.example.com',
|
|
uri: '/dashboard',
|
|
proto: 'HTTP/1.1',
|
|
},
|
|
status: 429,
|
|
duration: 0.001,
|
|
size: 256,
|
|
},
|
|
];
|
|
|
|
/**
|
|
* Generate mock log entries for pagination testing
|
|
*/
|
|
function generateMockEntries(count: number, startOffset: number = 0) {
|
|
return Array.from({ length: count }, (_, i) => ({
|
|
level: i % 3 === 0 ? 'error' : i % 3 === 1 ? 'warn' : 'info',
|
|
ts: Math.floor(Date.now() / 1000) - i * 10,
|
|
logger: 'http.log.access',
|
|
msg: `request ${startOffset + i}`,
|
|
request: {
|
|
remote_ip: `192.168.1.${100 + (i % 100)}`,
|
|
method: ['GET', 'POST', 'PUT'][i % 3],
|
|
host: 'example.com',
|
|
uri: `/api/endpoint-${i}`,
|
|
proto: 'HTTP/2',
|
|
},
|
|
status: 200 + (i % 4) * 100,
|
|
duration: Math.random() * 1,
|
|
size: Math.random() * 1000,
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Selectors and helpers for WebKit compatibility
|
|
*/
|
|
function getLogFileButton(page: Page, fileName: string) {
|
|
return page.getByTestId(`log-file-${fileName}`);
|
|
}
|
|
|
|
function getPrevButton(page: Page) {
|
|
return page.getByTestId('prev-page-button');
|
|
}
|
|
|
|
function getNextButton(page: Page) {
|
|
return page.getByTestId('next-page-button');
|
|
}
|
|
|
|
/**
|
|
* Helper to set up log API mocks
|
|
*/
|
|
async function setupLogMocks(
|
|
page: Page,
|
|
files = mockLogFiles,
|
|
entries = mockLogEntries,
|
|
totalCount?: number
|
|
) {
|
|
// Mock log files list endpoint
|
|
await page.route('**/api/v1/logs', async (route) => {
|
|
if (route.request().method() === 'GET') {
|
|
await route.fulfill({ status: 200, json: files });
|
|
} else {
|
|
await route.continue();
|
|
}
|
|
});
|
|
|
|
// Mock log content endpoint for each file
|
|
for (const file of files) {
|
|
await page.route(`**/api/v1/logs/${file.name}*`, async (route) => {
|
|
const url = new URL(route.request().url());
|
|
const offset = parseInt(url.searchParams.get('offset') || '0');
|
|
const limit = parseInt(url.searchParams.get('limit') || '50');
|
|
const search = url.searchParams.get('search') || '';
|
|
const level = url.searchParams.get('level') || '';
|
|
|
|
// Apply filters
|
|
let filtered = [...entries];
|
|
if (search) {
|
|
filtered = filtered.filter(
|
|
(e) =>
|
|
e.msg.toLowerCase().includes(search.toLowerCase()) ||
|
|
e.request.uri.toLowerCase().includes(search.toLowerCase())
|
|
);
|
|
}
|
|
if (level) {
|
|
filtered = filtered.filter((e) => e.level.toLowerCase() === level.toLowerCase());
|
|
}
|
|
|
|
const paginated = filtered.slice(offset, offset + limit);
|
|
const total = totalCount || filtered.length;
|
|
|
|
await route.fulfill({
|
|
status: 200,
|
|
json: {
|
|
filename: file.name,
|
|
logs: paginated,
|
|
total,
|
|
limit,
|
|
offset,
|
|
},
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
test.describe('Logs Page - WebKit Compatible Tests', () => {
|
|
test.describe('Page Layout', () => {
|
|
test('should display logs page with file selector', async ({ page, authenticatedUser }) => {
|
|
await loginUser(page, authenticatedUser);
|
|
await setupLogMocks(page);
|
|
|
|
await page.goto('/tasks/logs', { waitUntil: 'domcontentloaded' });
|
|
await waitForLoadingComplete(page);
|
|
|
|
// Verify page title contains "logs"
|
|
await expect(page.getByRole('heading', { level: 1 })).toContainText(/logs/i);
|
|
|
|
// Verify file list sidebar is visible
|
|
await expect(page.getByTestId('log-file-list')).toBeVisible();
|
|
});
|
|
|
|
test('should show list of available log files', async ({ page, authenticatedUser }) => {
|
|
await loginUser(page, authenticatedUser);
|
|
await setupLogMocks(page);
|
|
|
|
const logFilesPromise = waitForAPIResponse(page, '/api/v1/logs', { status: 200 });
|
|
await page.goto('/tasks/logs', { waitUntil: 'domcontentloaded' });
|
|
await waitForLoadingComplete(page);
|
|
|
|
await logFilesPromise;
|
|
|
|
// Verify all log files are displayed in the list
|
|
await expect(page.getByText('access.log')).toBeVisible();
|
|
await expect(page.getByText('error.log')).toBeVisible();
|
|
await expect(page.getByText('caddy.log')).toBeVisible();
|
|
});
|
|
|
|
test('should display log filters section', async ({ page, authenticatedUser }) => {
|
|
await loginUser(page, authenticatedUser);
|
|
await setupLogMocks(page);
|
|
|
|
await page.goto('/tasks/logs', { waitUntil: 'domcontentloaded' });
|
|
await waitForLoadingComplete(page);
|
|
|
|
// Wait for filters (appear when first log file is auto-selected)
|
|
await expect(page.getByTestId('search-input')).toBeVisible({ timeout: 5000 });
|
|
|
|
// Verify filter controls are present
|
|
await expect(page.getByTestId('refresh-button')).toBeVisible();
|
|
await expect(page.getByTestId('download-button')).toBeVisible();
|
|
});
|
|
});
|
|
|
|
test.describe('Log File List', () => {
|
|
test('should list all available log files with metadata', async ({ page, authenticatedUser }) => {
|
|
await loginUser(page, authenticatedUser);
|
|
await setupLogMocks(page);
|
|
|
|
await page.goto('/tasks/logs', { waitUntil: 'domcontentloaded' });
|
|
await waitForLoadingComplete(page);
|
|
|
|
// Verify files are listed with size information (in MB format)
|
|
await expect(page.getByText('access.log')).toBeVisible();
|
|
await expect(page.getByText('1.00 MB')).toBeVisible();
|
|
|
|
await expect(page.getByText('error.log')).toBeVisible();
|
|
await expect(page.getByText('0.24 MB')).toBeVisible();
|
|
});
|
|
|
|
test('should load log content when file selected', async ({ page, authenticatedUser }) => {
|
|
await loginUser(page, authenticatedUser);
|
|
await setupLogMocks(page);
|
|
|
|
const initialContentPromise = waitForAPIResponse(page, '/api/v1/logs/access.log', { status: 200 });
|
|
await page.goto('/tasks/logs', { waitUntil: 'domcontentloaded' });
|
|
await waitForLoadingComplete(page);
|
|
|
|
// The first file (access.log) is auto-selected - wait for content
|
|
await initialContentPromise;
|
|
|
|
// Verify log table is displayed
|
|
await expect(page.getByTestId('log-table')).toBeVisible();
|
|
});
|
|
});
|
|
|
|
test.describe('Log Content Display', () => {
|
|
test('should display log entries in table format', async ({ page, authenticatedUser }) => {
|
|
await loginUser(page, authenticatedUser);
|
|
await setupLogMocks(page);
|
|
|
|
const initialContentPromise = waitForAPIResponse(page, '/api/v1/logs/access.log', { status: 200 });
|
|
await page.goto('/tasks/logs', { waitUntil: 'domcontentloaded' });
|
|
await waitForLoadingComplete(page);
|
|
|
|
// Wait for auto-selected log content to load
|
|
await initialContentPromise;
|
|
|
|
// Verify table structure
|
|
const logTable = page.getByTestId('log-table');
|
|
await expect(logTable).toBeVisible();
|
|
|
|
// Verify table has expected columns
|
|
await expect(page.getByRole('columnheader', { name: /time/i })).toBeVisible();
|
|
await expect(page.getByRole('columnheader', { name: /status/i })).toBeVisible();
|
|
await expect(page.getByRole('columnheader', { name: /method/i })).toBeVisible();
|
|
});
|
|
|
|
test('should show timestamp, level, method, uri, status', async ({ page, authenticatedUser }) => {
|
|
await loginUser(page, authenticatedUser);
|
|
await setupLogMocks(page);
|
|
|
|
const initialContentPromise = waitForAPIResponse(page, '/api/v1/logs/access.log', { status: 200 });
|
|
await page.goto('/tasks/logs', { waitUntil: 'domcontentloaded' });
|
|
await waitForLoadingComplete(page);
|
|
|
|
// Wait for content to load
|
|
await initialContentPromise;
|
|
|
|
// Verify log entry content is displayed
|
|
// The mock data includes 192.168.1.100 as remote_ip in first entry
|
|
const entryRow = page.getByRole('row').filter({ hasText: '192.168.1.100' }).first();
|
|
await expect(entryRow).toBeVisible();
|
|
await expect(entryRow.getByRole('cell', { name: 'GET' })).toBeVisible();
|
|
await expect(entryRow.getByText('/api/v1/users')).toBeVisible();
|
|
await expect(entryRow.getByTestId('status-200')).toBeVisible();
|
|
});
|
|
|
|
test('should highlight error entries with distinct styling', async ({ page, authenticatedUser }) => {
|
|
await loginUser(page, authenticatedUser);
|
|
await setupLogMocks(page);
|
|
|
|
const initialContentPromise = waitForAPIResponse(page, '/api/v1/logs/access.log', { status: 200 });
|
|
await page.goto('/tasks/logs', { waitUntil: 'domcontentloaded' });
|
|
await waitForLoadingComplete(page);
|
|
await initialContentPromise;
|
|
|
|
// Find the 502 error status badge - should have red styling class
|
|
const errorStatus = page.getByTestId('status-502');
|
|
await expect(errorStatus).toBeVisible();
|
|
|
|
// Verify error has red styling (bg-red or similar)
|
|
await expect(errorStatus).toHaveClass(/red/);
|
|
});
|
|
});
|
|
|
|
test.describe('Pagination', () => {
|
|
test('should paginate large log files', async ({ page, authenticatedUser }) => {
|
|
await loginUser(page, authenticatedUser);
|
|
|
|
// Generate 150 mock entries for pagination testing
|
|
const largeEntrySet = generateMockEntries(150);
|
|
let capturedOffset = 0;
|
|
|
|
await page.route('**/api/v1/logs', async (route) => {
|
|
await route.fulfill({ status: 200, json: mockLogFiles });
|
|
});
|
|
|
|
await page.route('**/api/v1/logs/access.log*', async (route) => {
|
|
const url = new URL(route.request().url());
|
|
capturedOffset = parseInt(url.searchParams.get('offset') || '0');
|
|
const limit = parseInt(url.searchParams.get('limit') || '50');
|
|
|
|
await route.fulfill({
|
|
status: 200,
|
|
json: {
|
|
filename: 'access.log',
|
|
logs: largeEntrySet.slice(capturedOffset, capturedOffset + limit),
|
|
total: 150,
|
|
limit,
|
|
offset: capturedOffset,
|
|
},
|
|
});
|
|
});
|
|
|
|
// Set up the response wait BEFORE navigation to avoid missing fast mocked responses.
|
|
const initialContentPromise = waitForAPIResponse(page, '/api/v1/logs/access.log', { status: 200 });
|
|
await page.goto('/tasks/logs', { waitUntil: 'domcontentloaded' });
|
|
await waitForLoadingComplete(page);
|
|
await initialContentPromise;
|
|
|
|
// Initial state - page 1
|
|
expect(capturedOffset).toBe(0);
|
|
|
|
// Click next page button
|
|
const nextButton = getNextButton(page);
|
|
await expect(nextButton).toBeEnabled();
|
|
|
|
// Set up listener BEFORE clicking to capture the request
|
|
await Promise.all([
|
|
page.waitForResponse((resp) => resp.url().includes('/api/v1/logs/access.log') && resp.status() === 200),
|
|
nextButton.click(),
|
|
]);
|
|
|
|
// Should have requested offset 50 (second page)
|
|
expect(capturedOffset).toBe(50);
|
|
});
|
|
|
|
test('should display page info correctly', async ({ page, authenticatedUser }) => {
|
|
await loginUser(page, authenticatedUser);
|
|
|
|
const largeEntrySet = generateMockEntries(150);
|
|
|
|
await page.route('**/api/v1/logs', async (route) => {
|
|
await route.fulfill({ status: 200, json: mockLogFiles });
|
|
});
|
|
|
|
await page.route('**/api/v1/logs/access.log*', async (route) => {
|
|
const url = new URL(route.request().url());
|
|
const offset = parseInt(url.searchParams.get('offset') || '0');
|
|
const limit = parseInt(url.searchParams.get('limit') || '50');
|
|
|
|
await route.fulfill({
|
|
status: 200,
|
|
json: {
|
|
filename: 'access.log',
|
|
logs: largeEntrySet.slice(offset, offset + limit),
|
|
total: 150,
|
|
limit,
|
|
offset,
|
|
},
|
|
});
|
|
});
|
|
|
|
const initialContentPromise = waitForAPIResponse(page, '/api/v1/logs/access.log', { status: 200 });
|
|
await page.goto('/tasks/logs', { waitUntil: 'domcontentloaded' });
|
|
await waitForLoadingComplete(page);
|
|
await initialContentPromise;
|
|
|
|
// Verify page info displays correctly
|
|
const pageInfo = page.getByTestId('page-info');
|
|
await expect(pageInfo).toBeVisible();
|
|
|
|
// Should show "Showing 1 - 50 of 150" or similar format
|
|
await expect(pageInfo).toContainText(/1.*50.*150/);
|
|
});
|
|
|
|
test('should disable prev button on first page and next on last', async ({ page, authenticatedUser }) => {
|
|
await loginUser(page, authenticatedUser);
|
|
|
|
const entries = generateMockEntries(75); // 2 pages (50 + 25)
|
|
|
|
await page.route('**/api/v1/logs', async (route) => {
|
|
await route.fulfill({ status: 200, json: mockLogFiles });
|
|
});
|
|
|
|
await page.route('**/api/v1/logs/access.log*', async (route) => {
|
|
const url = new URL(route.request().url());
|
|
const offset = parseInt(url.searchParams.get('offset') || '0');
|
|
const limit = parseInt(url.searchParams.get('limit') || '50');
|
|
|
|
await route.fulfill({
|
|
status: 200,
|
|
json: {
|
|
filename: 'access.log',
|
|
logs: entries.slice(offset, offset + limit),
|
|
total: 75,
|
|
limit,
|
|
offset,
|
|
},
|
|
});
|
|
});
|
|
|
|
const initialContentPromise = waitForAPIResponse(page, '/api/v1/logs/access.log', { status: 200 });
|
|
await page.goto('/tasks/logs', { waitUntil: 'domcontentloaded' });
|
|
await waitForLoadingComplete(page);
|
|
await initialContentPromise;
|
|
|
|
const prevButton = getPrevButton(page);
|
|
const nextButton = getNextButton(page);
|
|
|
|
// On first page, prev should be disabled
|
|
await expect(prevButton).toBeDisabled();
|
|
await expect(nextButton).toBeEnabled();
|
|
|
|
// Navigate to last page
|
|
await Promise.all([
|
|
page.waitForResponse((resp) => resp.url().includes('/api/v1/logs/access.log')),
|
|
nextButton.click(),
|
|
]);
|
|
|
|
// On last page, next should be disabled
|
|
await expect(prevButton).toBeEnabled();
|
|
await expect(nextButton).toBeDisabled();
|
|
});
|
|
});
|
|
|
|
test.describe('Search and Filter', () => {
|
|
test('should filter logs by search text', async ({ page, authenticatedUser }) => {
|
|
await loginUser(page, authenticatedUser);
|
|
|
|
let capturedSearch = '';
|
|
|
|
await page.route('**/api/v1/logs', async (route) => {
|
|
await route.fulfill({ status: 200, json: mockLogFiles });
|
|
});
|
|
|
|
await page.route('**/api/v1/logs/access.log*', async (route) => {
|
|
const url = new URL(route.request().url());
|
|
capturedSearch = url.searchParams.get('search') || '';
|
|
|
|
// Filter mock entries based on search
|
|
const filtered = capturedSearch
|
|
? mockLogEntries.filter(
|
|
(e) =>
|
|
e.msg.toLowerCase().includes(capturedSearch.toLowerCase()) ||
|
|
e.request.uri.toLowerCase().includes(capturedSearch.toLowerCase())
|
|
)
|
|
: mockLogEntries;
|
|
|
|
await route.fulfill({
|
|
status: 200,
|
|
json: {
|
|
filename: 'access.log',
|
|
logs: filtered,
|
|
total: filtered.length,
|
|
limit: 50,
|
|
offset: 0,
|
|
},
|
|
});
|
|
});
|
|
|
|
const initialContentPromise = waitForAPIResponse(page, '/api/v1/logs/access.log', { status: 200 });
|
|
await page.goto('/tasks/logs', { waitUntil: 'domcontentloaded' });
|
|
await waitForLoadingComplete(page);
|
|
await initialContentPromise;
|
|
|
|
// Type in search input
|
|
const searchInput = page.getByTestId('search-input');
|
|
|
|
// Set up response listener BEFORE typing to catch the debounced request
|
|
const searchResponsePromise = page.waitForResponse((resp) =>
|
|
resp.url().includes('/api/v1/logs/access.log')
|
|
);
|
|
|
|
await searchInput.fill('users');
|
|
|
|
// Wait for debounced search request
|
|
await searchResponsePromise;
|
|
|
|
// Verify search parameter was sent
|
|
expect(capturedSearch).toBe('users');
|
|
});
|
|
|
|
test('should filter logs by log level', async ({ page, authenticatedUser }) => {
|
|
await loginUser(page, authenticatedUser);
|
|
|
|
let capturedLevel = '';
|
|
|
|
await page.route('**/api/v1/logs', async (route) => {
|
|
await route.fulfill({ status: 200, json: mockLogFiles });
|
|
});
|
|
|
|
await page.route('**/api/v1/logs/access.log*', async (route) => {
|
|
const url = new URL(route.request().url());
|
|
capturedLevel = url.searchParams.get('level') || '';
|
|
|
|
// Filter mock entries based on level
|
|
const filtered = capturedLevel
|
|
? mockLogEntries.filter((e) => e.level.toLowerCase() === capturedLevel.toLowerCase())
|
|
: mockLogEntries;
|
|
|
|
await route.fulfill({
|
|
status: 200,
|
|
json: {
|
|
filename: 'access.log',
|
|
logs: filtered,
|
|
total: filtered.length,
|
|
limit: 50,
|
|
offset: 0,
|
|
},
|
|
});
|
|
});
|
|
|
|
const initialContentPromise = waitForAPIResponse(page, '/api/v1/logs/access.log', { status: 200 });
|
|
await page.goto('/tasks/logs', { waitUntil: 'domcontentloaded' });
|
|
await waitForLoadingComplete(page);
|
|
await initialContentPromise;
|
|
|
|
// Select Error level from dropdown using data-testid
|
|
const levelSelect = page.getByTestId('level-select');
|
|
await expect(levelSelect).toBeVisible();
|
|
|
|
// Set up response listener BEFORE selecting
|
|
const filterResponsePromise = page.waitForResponse((resp) =>
|
|
resp.url().includes('/api/v1/logs/access.log')
|
|
);
|
|
|
|
await levelSelect.selectOption('ERROR');
|
|
await filterResponsePromise;
|
|
|
|
// Verify level parameter was sent
|
|
expect(capturedLevel.toLowerCase()).toBe('error');
|
|
});
|
|
});
|
|
|
|
test.describe('Download', () => {
|
|
test('should download log file successfully', async ({ page, authenticatedUser }) => {
|
|
await loginUser(page, authenticatedUser);
|
|
await setupLogMocks(page);
|
|
|
|
const initialContentPromise = waitForAPIResponse(page, '/api/v1/logs/access.log', { status: 200 });
|
|
await page.goto('/tasks/logs', { waitUntil: 'domcontentloaded' });
|
|
await waitForLoadingComplete(page);
|
|
await initialContentPromise;
|
|
|
|
// Verify download button is visible and enabled
|
|
const downloadButton = page.getByTestId('download-button');
|
|
await expect(downloadButton).toBeVisible();
|
|
await expect(downloadButton).toBeEnabled();
|
|
|
|
// The download button clicking will use window.location.href for download
|
|
// Verify the button state is correct for a successful download
|
|
await expect(downloadButton).not.toHaveAttribute('disabled');
|
|
});
|
|
|
|
test('should handle download error gracefully', async ({ page, authenticatedUser }) => {
|
|
await loginUser(page, authenticatedUser);
|
|
|
|
await page.route('**/api/v1/logs', async (route) => {
|
|
await route.fulfill({ status: 200, json: mockLogFiles });
|
|
});
|
|
|
|
await page.route('**/api/v1/logs/access.log*', async (route) => {
|
|
if (!route.request().url().includes('/download')) {
|
|
await route.fulfill({
|
|
status: 200,
|
|
json: {
|
|
filename: 'access.log',
|
|
logs: mockLogEntries,
|
|
total: mockLogEntries.length,
|
|
limit: 50,
|
|
offset: 0,
|
|
},
|
|
});
|
|
} else {
|
|
await route.continue();
|
|
}
|
|
});
|
|
|
|
const initialContentPromise = waitForAPIResponse(page, '/api/v1/logs/access.log', { status: 200 });
|
|
await page.goto('/tasks/logs', { waitUntil: 'domcontentloaded' });
|
|
await waitForLoadingComplete(page);
|
|
await initialContentPromise;
|
|
|
|
// Verify download button is present and properly rendered
|
|
const downloadButton = page.getByTestId('download-button');
|
|
await expect(downloadButton).toBeVisible();
|
|
|
|
// Button should be in a clickable state even if download endpoint fails
|
|
await expect(downloadButton).toBeEnabled();
|
|
});
|
|
});
|
|
|
|
test.describe('Edge Cases', () => {
|
|
test('should handle empty log content gracefully', async ({ page, authenticatedUser }) => {
|
|
await loginUser(page, authenticatedUser);
|
|
|
|
await page.route('**/api/v1/logs', async (route) => {
|
|
await route.fulfill({ status: 200, json: mockLogFiles });
|
|
});
|
|
|
|
await page.route('**/api/v1/logs/access.log*', async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
json: {
|
|
filename: 'access.log',
|
|
logs: [],
|
|
total: 0,
|
|
limit: 50,
|
|
offset: 0,
|
|
},
|
|
});
|
|
});
|
|
|
|
const initialContentPromise = waitForAPIResponse(page, '/api/v1/logs/access.log', { status: 200 });
|
|
await page.goto('/tasks/logs', { waitUntil: 'domcontentloaded' });
|
|
await waitForLoadingComplete(page);
|
|
await initialContentPromise;
|
|
|
|
// Should show "No logs found" or similar message
|
|
await expect(page.getByText(/no logs found|no.*matching/i)).toBeVisible();
|
|
});
|
|
|
|
test('should reset to first page when changing log file', async ({ page, authenticatedUser }) => {
|
|
await loginUser(page, authenticatedUser);
|
|
|
|
const largeEntrySet = generateMockEntries(150);
|
|
let lastOffset = 0;
|
|
|
|
await page.route('**/api/v1/logs', async (route) => {
|
|
await route.fulfill({ status: 200, json: mockLogFiles });
|
|
});
|
|
|
|
await page.route('**/api/v1/logs/*', async (route) => {
|
|
const url = new URL(route.request().url());
|
|
lastOffset = parseInt(url.searchParams.get('offset') || '0');
|
|
const limit = parseInt(url.searchParams.get('limit') || '50');
|
|
|
|
await route.fulfill({
|
|
status: 200,
|
|
json: {
|
|
filename: 'test.log',
|
|
logs: largeEntrySet.slice(lastOffset, lastOffset + limit),
|
|
total: 150,
|
|
limit,
|
|
offset: lastOffset,
|
|
},
|
|
});
|
|
});
|
|
|
|
await page.goto('/tasks/logs', { waitUntil: 'domcontentloaded' });
|
|
await waitForLoadingComplete(page);
|
|
|
|
// Navigate to page 2
|
|
const nextButton = getNextButton(page);
|
|
await nextButton.click();
|
|
|
|
// Wait briefly for state update
|
|
await page.waitForTimeout(500);
|
|
|
|
expect(lastOffset).toBe(50);
|
|
|
|
// Switch to different log file using the new data-testid
|
|
const errorLogButton = getLogFileButton(page, 'error.log');
|
|
await Promise.all([
|
|
page.waitForResponse((resp) => resp.url().includes('/api/v1/logs/')),
|
|
errorLogButton.click(),
|
|
]);
|
|
|
|
// Should reset to offset 0 when switching files
|
|
expect(lastOffset).toBe(0);
|
|
});
|
|
});
|
|
});
|