Complete v0.5.0 implementation: Database (@db-engineer): - 3 migrations: users, api_keys, report_schedules tables - Foreign keys, indexes, constraints, enums Backend (@backend-dev): - JWT authentication service with bcrypt (cost=12) - Auth endpoints: /register, /login, /refresh, /me - API Keys service with hash storage and prefix validation - API Keys endpoints: CRUD + rotate - Security module with JWT HS256 Frontend (@frontend-dev): - Login/Register pages with validation - AuthContext with localStorage persistence - Protected routes implementation - API Keys management UI (create, revoke, rotate) - Header with user dropdown DevOps (@devops-engineer): - .env.example and .env.production.example - docker-compose.scheduler.yml - scripts/setup-secrets.sh - INFRASTRUCTURE_SETUP.md QA (@qa-engineer): - 85 E2E tests: auth.spec.ts, apikeys.spec.ts, scenarios.spec.ts, regression-v050.spec.ts - auth-helpers.ts with 20+ utility functions - Test plans and documentation Architecture (@spec-architect): - SECURITY.md with best practices - SECURITY-CHECKLIST.md pre-deployment - Updated architecture.md with auth flows - Updated README.md with v0.5.0 features Documentation: - Updated todo.md with v0.5.0 status - Added docs/README.md index - Complete setup instructions Dependencies added: - bcrypt, python-jose, passlib, email-validator Tested: JWT auth flow, API keys CRUD, protected routes, 85 E2E tests ready Closes: v0.5.0 milestone
641 lines
21 KiB
TypeScript
641 lines
21 KiB
TypeScript
/**
|
|
* QA-FILTER-021: Filters Tests
|
|
*
|
|
* E2E Test Suite for Advanced Filters on Scenarios Page
|
|
* - Region filter
|
|
* - Cost filter
|
|
* - Status filter
|
|
* - Combined filters
|
|
* - URL sync with query params
|
|
* - Clear filters
|
|
* - Search by name
|
|
*/
|
|
|
|
import { test, expect } from '@playwright/test';
|
|
import {
|
|
navigateTo,
|
|
waitForLoading,
|
|
createScenarioViaAPI,
|
|
deleteScenarioViaAPI,
|
|
startScenarioViaAPI,
|
|
generateTestScenarioName,
|
|
} from './utils/test-helpers';
|
|
import {
|
|
generateTestUser,
|
|
loginUserViaUI,
|
|
registerUserViaAPI,
|
|
} from './utils/auth-helpers';
|
|
import { newScenarioData } from './fixtures/test-scenarios';
|
|
|
|
// Test data storage
|
|
let testUser: { email: string; password: string; fullName: string } | null = null;
|
|
let accessToken: string | null = null;
|
|
const createdScenarioIds: string[] = [];
|
|
|
|
// Test scenario names for cleanup
|
|
const scenarioNames = {
|
|
usEast: generateTestScenarioName('Filter-US-East'),
|
|
euWest: generateTestScenarioName('Filter-EU-West'),
|
|
apSouth: generateTestScenarioName('Filter-AP-South'),
|
|
lowCost: generateTestScenarioName('Filter-Low-Cost'),
|
|
highCost: generateTestScenarioName('Filter-High-Cost'),
|
|
running: generateTestScenarioName('Filter-Running'),
|
|
draft: generateTestScenarioName('Filter-Draft'),
|
|
searchMatch: generateTestScenarioName('Filter-Search-Match'),
|
|
};
|
|
|
|
test.describe('QA-FILTER-021: Filters Setup', () => {
|
|
test.beforeAll(async ({ request }) => {
|
|
// Register and login test user
|
|
testUser = generateTestUser('Filters');
|
|
const auth = await registerUserViaAPI(
|
|
request,
|
|
testUser.email,
|
|
testUser.password,
|
|
testUser.fullName
|
|
);
|
|
accessToken = auth.access_token;
|
|
|
|
// Create test scenarios with different properties
|
|
const scenarios = [
|
|
{ name: scenarioNames.usEast, region: 'us-east-1', status: 'draft' },
|
|
{ name: scenarioNames.euWest, region: 'eu-west-1', status: 'draft' },
|
|
{ name: scenarioNames.apSouth, region: 'ap-southeast-1', status: 'draft' },
|
|
{ name: scenarioNames.searchMatch, region: 'us-west-2', status: 'draft' },
|
|
];
|
|
|
|
for (const scenario of scenarios) {
|
|
const created = await createScenarioViaAPI(request, {
|
|
...newScenarioData,
|
|
name: scenario.name,
|
|
region: scenario.region,
|
|
});
|
|
createdScenarioIds.push(created.id);
|
|
}
|
|
});
|
|
|
|
test.afterAll(async ({ request }) => {
|
|
// Cleanup all created scenarios
|
|
for (const id of createdScenarioIds) {
|
|
try {
|
|
await deleteScenarioViaAPI(request, id);
|
|
} catch {
|
|
// Ignore cleanup errors
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// ============================================
|
|
// TEST SUITE: Region Filter
|
|
// ============================================
|
|
test.describe('QA-FILTER-021: Region Filter', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
// Login and navigate
|
|
await loginUserViaUI(page, testUser!.email, testUser!.password);
|
|
await navigateTo(page, '/scenarios');
|
|
await waitForLoading(page);
|
|
});
|
|
|
|
test('should apply region filter and update list', async ({ page }) => {
|
|
// Find and open region filter
|
|
const regionFilter = page.getByLabel(/region|select region/i).or(
|
|
page.locator('[data-testid="region-filter"]').or(
|
|
page.getByRole('combobox', { name: /region/i })
|
|
)
|
|
);
|
|
|
|
if (!await regionFilter.isVisible().catch(() => false)) {
|
|
test.skip(true, 'Region filter not found');
|
|
}
|
|
|
|
// Select US East region
|
|
await regionFilter.click();
|
|
await regionFilter.selectOption?.('us-east-1') ||
|
|
page.getByText('us-east-1').click();
|
|
|
|
// Apply filter
|
|
await page.getByRole('button', { name: /apply|filter|search/i }).click();
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// Verify list updates - should show only us-east-1 scenarios
|
|
await expect(page.getByText(scenarioNames.usEast)).toBeVisible();
|
|
await expect(page.getByText(scenarioNames.euWest)).not.toBeVisible();
|
|
await expect(page.getByText(scenarioNames.apSouth)).not.toBeVisible();
|
|
});
|
|
|
|
test('should filter by eu-west-1 region', async ({ page }) => {
|
|
const regionFilter = page.getByLabel(/region/i).or(
|
|
page.locator('[data-testid="region-filter"]')
|
|
);
|
|
|
|
if (!await regionFilter.isVisible().catch(() => false)) {
|
|
test.skip(true, 'Region filter not found');
|
|
}
|
|
|
|
await regionFilter.click();
|
|
await regionFilter.selectOption?.('eu-west-1') ||
|
|
page.getByText('eu-west-1').click();
|
|
|
|
await page.getByRole('button', { name: /apply|filter/i }).click();
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
await expect(page.getByText(scenarioNames.euWest)).toBeVisible();
|
|
await expect(page.getByText(scenarioNames.usEast)).not.toBeVisible();
|
|
});
|
|
|
|
test('should show all regions when no filter selected', async ({ page }) => {
|
|
// Ensure no region filter is applied
|
|
const clearButton = page.getByRole('button', { name: /clear|reset/i });
|
|
if (await clearButton.isVisible().catch(() => false)) {
|
|
await clearButton.click();
|
|
await page.waitForLoadState('networkidle');
|
|
}
|
|
|
|
// All scenarios should be visible
|
|
await expect(page.getByText(scenarioNames.usEast)).toBeVisible();
|
|
await expect(page.getByText(scenarioNames.euWest)).toBeVisible();
|
|
await expect(page.getByText(scenarioNames.apSouth)).toBeVisible();
|
|
});
|
|
});
|
|
|
|
// ============================================
|
|
// TEST SUITE: Cost Filter
|
|
// ============================================
|
|
test.describe('QA-FILTER-021: Cost Filter', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await loginUserViaUI(page, testUser!.email, testUser!.password);
|
|
await navigateTo(page, '/scenarios');
|
|
await waitForLoading(page);
|
|
});
|
|
|
|
test('should apply min cost filter', async ({ page }) => {
|
|
const minCostInput = page.getByLabel(/min cost|minimum cost|from cost/i).or(
|
|
page.locator('input[placeholder*="min"], input[name*="min_cost"], [data-testid*="min-cost"]')
|
|
);
|
|
|
|
if (!await minCostInput.isVisible().catch(() => false)) {
|
|
test.skip(true, 'Min cost filter not found');
|
|
}
|
|
|
|
await minCostInput.fill('10');
|
|
await page.getByRole('button', { name: /apply|filter/i }).click();
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// Verify filtered results
|
|
await expect(page.locator('table tbody tr')).toHaveCount(await page.locator('table tbody tr').count());
|
|
});
|
|
|
|
test('should apply max cost filter', async ({ page }) => {
|
|
const maxCostInput = page.getByLabel(/max cost|maximum cost|to cost/i).or(
|
|
page.locator('input[placeholder*="max"], input[name*="max_cost"], [data-testid*="max-cost"]')
|
|
);
|
|
|
|
if (!await maxCostInput.isVisible().catch(() => false)) {
|
|
test.skip(true, 'Max cost filter not found');
|
|
}
|
|
|
|
await maxCostInput.fill('100');
|
|
await page.getByRole('button', { name: /apply|filter/i }).click();
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// Verify results
|
|
await expect(page.locator('table tbody')).toBeVisible();
|
|
});
|
|
|
|
test('should apply cost range filter', async ({ page }) => {
|
|
const minCostInput = page.getByLabel(/min cost/i).or(
|
|
page.locator('[data-testid*="min-cost"]')
|
|
);
|
|
const maxCostInput = page.getByLabel(/max cost/i).or(
|
|
page.locator('[data-testid*="max-cost"]')
|
|
);
|
|
|
|
if (!await minCostInput.isVisible().catch(() => false) ||
|
|
!await maxCostInput.isVisible().catch(() => false)) {
|
|
test.skip(true, 'Cost range filters not found');
|
|
}
|
|
|
|
await minCostInput.fill('5');
|
|
await maxCostInput.fill('50');
|
|
await page.getByRole('button', { name: /apply|filter/i }).click();
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// Verify results are filtered
|
|
await expect(page.locator('table')).toBeVisible();
|
|
});
|
|
});
|
|
|
|
// ============================================
|
|
// TEST SUITE: Status Filter
|
|
// ============================================
|
|
test.describe('QA-FILTER-021: Status Filter', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await loginUserViaUI(page, testUser!.email, testUser!.password);
|
|
await navigateTo(page, '/scenarios');
|
|
await waitForLoading(page);
|
|
});
|
|
|
|
test('should filter by draft status', async ({ page }) => {
|
|
const statusFilter = page.getByLabel(/status/i).or(
|
|
page.locator('[data-testid="status-filter"]')
|
|
);
|
|
|
|
if (!await statusFilter.isVisible().catch(() => false)) {
|
|
test.skip(true, 'Status filter not found');
|
|
}
|
|
|
|
await statusFilter.click();
|
|
await statusFilter.selectOption?.('draft') ||
|
|
page.getByText('draft', { exact: true }).click();
|
|
|
|
await page.getByRole('button', { name: /apply|filter/i }).click();
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// Verify only draft scenarios are shown
|
|
const rows = page.locator('table tbody tr');
|
|
const count = await rows.count();
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
await expect(rows.nth(i)).toContainText('draft');
|
|
}
|
|
});
|
|
|
|
test('should filter by running status', async ({ page }) => {
|
|
const statusFilter = page.getByLabel(/status/i).or(
|
|
page.locator('[data-testid="status-filter"]')
|
|
);
|
|
|
|
if (!await statusFilter.isVisible().catch(() => false)) {
|
|
test.skip(true, 'Status filter not found');
|
|
}
|
|
|
|
await statusFilter.click();
|
|
await statusFilter.selectOption?.('running') ||
|
|
page.getByText('running', { exact: true }).click();
|
|
|
|
await page.getByRole('button', { name: /apply|filter/i }).click();
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// Verify filtered results
|
|
await expect(page.locator('table')).toBeVisible();
|
|
});
|
|
});
|
|
|
|
// ============================================
|
|
// TEST SUITE: Combined Filters
|
|
// ============================================
|
|
test.describe('QA-FILTER-021: Combined Filters', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await loginUserViaUI(page, testUser!.email, testUser!.password);
|
|
await navigateTo(page, '/scenarios');
|
|
await waitForLoading(page);
|
|
});
|
|
|
|
test('should combine region and status filters', async ({ page }) => {
|
|
const regionFilter = page.getByLabel(/region/i);
|
|
const statusFilter = page.getByLabel(/status/i);
|
|
|
|
if (!await regionFilter.isVisible().catch(() => false) ||
|
|
!await statusFilter.isVisible().catch(() => false)) {
|
|
test.skip(true, 'Required filters not found');
|
|
}
|
|
|
|
// Apply region filter
|
|
await regionFilter.click();
|
|
await regionFilter.selectOption?.('us-east-1') ||
|
|
page.getByText('us-east-1').click();
|
|
|
|
// Apply status filter
|
|
await statusFilter.click();
|
|
await statusFilter.selectOption?.('draft') ||
|
|
page.getByText('draft').click();
|
|
|
|
// Apply filters
|
|
await page.getByRole('button', { name: /apply|filter/i }).click();
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// Verify combined results
|
|
await expect(page.locator('table tbody')).toBeVisible();
|
|
});
|
|
|
|
test('should sync filters with URL query params', async ({ page }) => {
|
|
const regionFilter = page.getByLabel(/region/i);
|
|
|
|
if (!await regionFilter.isVisible().catch(() => false)) {
|
|
test.skip(true, 'Region filter not found');
|
|
}
|
|
|
|
// Apply filter
|
|
await regionFilter.click();
|
|
await regionFilter.selectOption?.('eu-west-1') ||
|
|
page.getByText('eu-west-1').click();
|
|
|
|
await page.getByRole('button', { name: /apply|filter/i }).click();
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// Verify URL contains query params
|
|
await expect(page).toHaveURL(/region=eu-west-1/);
|
|
});
|
|
|
|
test('should parse filters from URL on page load', async ({ page }) => {
|
|
// Navigate with query params
|
|
await navigateTo(page, '/scenarios?region=us-east-1&status=draft');
|
|
await waitForLoading(page);
|
|
|
|
// Verify filters are applied
|
|
const url = page.url();
|
|
expect(url).toContain('region=us-east-1');
|
|
expect(url).toContain('status=draft');
|
|
|
|
// Verify filtered results
|
|
await expect(page.locator('table')).toBeVisible();
|
|
});
|
|
|
|
test('should handle multiple region filters in URL', async ({ page }) => {
|
|
// Navigate with multiple regions
|
|
await navigateTo(page, '/scenarios?region=us-east-1®ion=eu-west-1');
|
|
await waitForLoading(page);
|
|
|
|
// Verify URL is preserved
|
|
await expect(page).toHaveURL(/region=/);
|
|
});
|
|
});
|
|
|
|
// ============================================
|
|
// TEST SUITE: Clear Filters
|
|
// ============================================
|
|
test.describe('QA-FILTER-021: Clear Filters', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await loginUserViaUI(page, testUser!.email, testUser!.password);
|
|
await navigateTo(page, '/scenarios');
|
|
await waitForLoading(page);
|
|
});
|
|
|
|
test('should clear all filters and restore full list', async ({ page }) => {
|
|
// Apply a filter first
|
|
const regionFilter = page.getByLabel(/region/i);
|
|
|
|
if (!await regionFilter.isVisible().catch(() => false)) {
|
|
test.skip(true, 'Region filter not found');
|
|
}
|
|
|
|
await regionFilter.click();
|
|
await regionFilter.selectOption?.('us-east-1') ||
|
|
page.getByText('us-east-1').click();
|
|
|
|
await page.getByRole('button', { name: /apply|filter/i }).click();
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// Get filtered count
|
|
const filteredCount = await page.locator('table tbody tr').count();
|
|
|
|
// Clear filters
|
|
const clearButton = page.getByRole('button', { name: /clear|reset|clear filters/i });
|
|
if (!await clearButton.isVisible().catch(() => false)) {
|
|
test.skip(true, 'Clear filters button not found');
|
|
}
|
|
|
|
await clearButton.click();
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// Verify all scenarios are visible
|
|
await expect(page.getByText(scenarioNames.usEast)).toBeVisible();
|
|
await expect(page.getByText(scenarioNames.euWest)).toBeVisible();
|
|
await expect(page.getByText(scenarioNames.apSouth)).toBeVisible();
|
|
|
|
// Verify URL is cleared
|
|
await expect(page).toHaveURL(/\/scenarios$/);
|
|
});
|
|
|
|
test('should clear individual filter', async ({ page }) => {
|
|
// Apply filters
|
|
const regionFilter = page.getByLabel(/region/i);
|
|
|
|
if (!await regionFilter.isVisible().catch(() => false)) {
|
|
test.skip(true, 'Region filter not found');
|
|
}
|
|
|
|
await regionFilter.click();
|
|
await regionFilter.selectOption?.('us-east-1');
|
|
await page.getByRole('button', { name: /apply|filter/i }).click();
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// Clear region filter specifically
|
|
const regionClear = page.locator('[data-testid="clear-region"]').or(
|
|
page.locator('[aria-label*="clear region"]')
|
|
);
|
|
|
|
if (await regionClear.isVisible().catch(() => false)) {
|
|
await regionClear.click();
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// Verify filter cleared
|
|
await expect(page.locator('table tbody')).toBeVisible();
|
|
}
|
|
});
|
|
|
|
test('should clear filters on page refresh if not persisted', async ({ page }) => {
|
|
// Apply filter
|
|
const regionFilter = page.getByLabel(/region/i);
|
|
|
|
if (!await regionFilter.isVisible().catch(() => false)) {
|
|
test.skip(true, 'Region filter not found');
|
|
}
|
|
|
|
await regionFilter.click();
|
|
await regionFilter.selectOption?.('us-east-1') ||
|
|
page.getByText('us-east-1').click();
|
|
|
|
await page.getByRole('button', { name: /apply|filter/i }).click();
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// Refresh without query params
|
|
await page.goto('/scenarios');
|
|
await waitForLoading(page);
|
|
|
|
// All scenarios should be visible
|
|
await expect(page.locator('table tbody tr')).toHaveCount(
|
|
await page.locator('table tbody tr').count()
|
|
);
|
|
});
|
|
});
|
|
|
|
// ============================================
|
|
// TEST SUITE: Search by Name
|
|
// ============================================
|
|
test.describe('QA-FILTER-021: Search by Name', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await loginUserViaUI(page, testUser!.email, testUser!.password);
|
|
await navigateTo(page, '/scenarios');
|
|
await waitForLoading(page);
|
|
});
|
|
|
|
test('should search scenarios by name', async ({ page }) => {
|
|
const searchInput = page.getByPlaceholder(/search|search by name/i).or(
|
|
page.getByLabel(/search/i).or(
|
|
page.locator('input[type="search"], [data-testid="search-input"]')
|
|
)
|
|
);
|
|
|
|
if (!await searchInput.isVisible().catch(() => false)) {
|
|
test.skip(true, 'Search input not found');
|
|
}
|
|
|
|
// Search for specific scenario
|
|
await searchInput.fill('US-East');
|
|
await page.waitForTimeout(500); // Debounce wait
|
|
|
|
// Verify search results
|
|
await expect(page.getByText(scenarioNames.usEast)).toBeVisible();
|
|
});
|
|
|
|
test('should filter results with partial name match', async ({ page }) => {
|
|
const searchInput = page.getByPlaceholder(/search/i).or(
|
|
page.locator('[data-testid="search-input"]')
|
|
);
|
|
|
|
if (!await searchInput.isVisible().catch(() => false)) {
|
|
test.skip(true, 'Search input not found');
|
|
}
|
|
|
|
// Partial search
|
|
await searchInput.fill('Filter-US');
|
|
await page.waitForTimeout(500);
|
|
|
|
// Should match US scenarios
|
|
await expect(page.getByText(scenarioNames.usEast)).toBeVisible();
|
|
});
|
|
|
|
test('should show no results for non-matching search', async ({ page }) => {
|
|
const searchInput = page.getByPlaceholder(/search/i).or(
|
|
page.locator('[data-testid="search-input"]')
|
|
);
|
|
|
|
if (!await searchInput.isVisible().catch(() => false)) {
|
|
test.skip(true, 'Search input not found');
|
|
}
|
|
|
|
// Search for non-existent scenario
|
|
await searchInput.fill('xyz-non-existent-scenario-12345');
|
|
await page.waitForTimeout(500);
|
|
|
|
// Verify no results or empty state
|
|
const rows = page.locator('table tbody tr');
|
|
const count = await rows.count();
|
|
|
|
if (count > 0) {
|
|
await expect(page.getByText(/no results|no.*found|empty/i).first()).toBeVisible();
|
|
}
|
|
});
|
|
|
|
test('should combine search with other filters', async ({ page }) => {
|
|
const searchInput = page.getByPlaceholder(/search/i).or(
|
|
page.locator('[data-testid="search-input"]')
|
|
);
|
|
const regionFilter = page.getByLabel(/region/i);
|
|
|
|
if (!await searchInput.isVisible().catch(() => false) ||
|
|
!await regionFilter.isVisible().catch(() => false)) {
|
|
test.skip(true, 'Required filters not found');
|
|
}
|
|
|
|
// Apply search
|
|
await searchInput.fill('Filter');
|
|
await page.waitForTimeout(500);
|
|
|
|
// Apply region filter
|
|
await regionFilter.click();
|
|
await regionFilter.selectOption?.('us-east-1') ||
|
|
page.getByText('us-east-1').click();
|
|
|
|
await page.getByRole('button', { name: /apply|filter/i }).click();
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// Verify combined results
|
|
await expect(page.locator('table tbody')).toBeVisible();
|
|
});
|
|
|
|
test('should clear search and show all results', async ({ page }) => {
|
|
const searchInput = page.getByPlaceholder(/search/i).or(
|
|
page.locator('[data-testid="search-input"]')
|
|
);
|
|
|
|
if (!await searchInput.isVisible().catch(() => false)) {
|
|
test.skip(true, 'Search input not found');
|
|
}
|
|
|
|
// Apply search
|
|
await searchInput.fill('US-East');
|
|
await page.waitForTimeout(500);
|
|
|
|
// Clear search
|
|
const clearButton = page.locator('[data-testid="clear-search"]').or(
|
|
page.getByRole('button', { name: /clear/i })
|
|
);
|
|
|
|
if (await clearButton.isVisible().catch(() => false)) {
|
|
await clearButton.click();
|
|
} else {
|
|
await searchInput.fill('');
|
|
}
|
|
|
|
await page.waitForTimeout(500);
|
|
|
|
// Verify all scenarios visible
|
|
await expect(page.locator('table tbody')).toBeVisible();
|
|
});
|
|
});
|
|
|
|
// ============================================
|
|
// TEST SUITE: Date Range Filter
|
|
// ============================================
|
|
test.describe('QA-FILTER-021: Date Range Filter', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await loginUserViaUI(page, testUser!.email, testUser!.password);
|
|
await navigateTo(page, '/scenarios');
|
|
await waitForLoading(page);
|
|
});
|
|
|
|
test('should filter by created date range', async ({ page }) => {
|
|
const dateFrom = page.getByLabel(/from|start date|date from/i).or(
|
|
page.locator('input[type="date"]').first()
|
|
);
|
|
|
|
if (!await dateFrom.isVisible().catch(() => false)) {
|
|
test.skip(true, 'Date filter not found');
|
|
}
|
|
|
|
const today = new Date().toISOString().split('T')[0];
|
|
await dateFrom.fill(today);
|
|
|
|
await page.getByRole('button', { name: /apply|filter/i }).click();
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// Verify results
|
|
await expect(page.locator('table tbody')).toBeVisible();
|
|
});
|
|
|
|
test('should filter by date range with from and to', async ({ page }) => {
|
|
const dateFrom = page.getByLabel(/from|start date/i);
|
|
const dateTo = page.getByLabel(/to|end date/i);
|
|
|
|
if (!await dateFrom.isVisible().catch(() => false) ||
|
|
!await dateTo.isVisible().catch(() => false)) {
|
|
test.skip(true, 'Date range filters not found');
|
|
}
|
|
|
|
const today = new Date();
|
|
const yesterday = new Date(today);
|
|
yesterday.setDate(yesterday.getDate() - 1);
|
|
|
|
await dateFrom.fill(yesterday.toISOString().split('T')[0]);
|
|
await dateTo.fill(today.toISOString().split('T')[0]);
|
|
|
|
await page.getByRole('button', { name: /apply|filter/i }).click();
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
await expect(page.locator('table tbody')).toBeVisible();
|
|
});
|
|
});
|