/** * Issue Management E2E Tests * * Tests for the issue list and detail pages. */ import { test, expect } from '@playwright/test'; test.describe('Issue Management', () => { // Use a test project ID const projectId = 'test-project-123'; test.beforeEach(async ({ page }) => { // Mock authentication - inject test auth store await page.addInitScript(() => { window.__TEST_AUTH_STORE__ = { getState: () => ({ isAuthenticated: true, user: { id: 'test-user', email: 'test@example.com', is_superuser: false }, accessToken: 'test-token', refreshToken: 'test-refresh', }), }; }); }); test.describe('Issue List Page', () => { test('displays issues list', async ({ page }) => { await page.goto(`/en/projects/${projectId}/issues`); // Wait for the page to load await expect(page.getByRole('heading', { name: /issues/i })).toBeVisible(); // Should show issue count await expect(page.getByText(/issues found/i)).toBeVisible(); }); test('has search functionality', async ({ page }) => { await page.goto(`/en/projects/${projectId}/issues`); const searchInput = page.getByPlaceholder('Search issues...'); await expect(searchInput).toBeVisible(); // Type in search await searchInput.fill('authentication'); // Wait for debounced search (mock data should filter) await page.waitForTimeout(500); }); test('has status filter', async ({ page }) => { await page.goto(`/en/projects/${projectId}/issues`); // Find status filter const statusFilter = page.getByRole('combobox', { name: /filter by status/i }); await expect(statusFilter).toBeVisible(); // Open and select a status await statusFilter.click(); await page.getByRole('option', { name: /in progress/i }).click(); }); test('can toggle extended filters', async ({ page }) => { await page.goto(`/en/projects/${projectId}/issues`); // Extended filters should not be visible initially await expect(page.getByLabel('Priority')).not.toBeVisible(); // Click filter toggle await page.getByRole('button', { name: /toggle extended filters/i }).click(); // Extended filters should now be visible await expect(page.getByLabel('Priority')).toBeVisible(); await expect(page.getByLabel('Sprint')).toBeVisible(); await expect(page.getByLabel('Assignee')).toBeVisible(); }); test('can select issues for bulk actions', async ({ page }) => { await page.goto(`/en/projects/${projectId}/issues`); // Wait for issues to load await page.waitForSelector('[data-testid^="issue-row-"]'); // Select first issue checkbox const firstCheckbox = page.getByRole('checkbox', { name: /select issue/i }).first(); await firstCheckbox.click(); // Bulk actions bar should appear await expect(page.getByText('1 selected')).toBeVisible(); await expect(page.getByRole('button', { name: /change status/i })).toBeVisible(); }); test('navigates to issue detail when clicking row', async ({ page }) => { await page.goto(`/en/projects/${projectId}/issues`); // Wait for issues to load await page.waitForSelector('[data-testid^="issue-row-"]'); // Click on first issue row await page.locator('[data-testid^="issue-row-"]').first().click(); // Should navigate to detail page await expect(page).toHaveURL(/\/issues\/[^/]+$/); }); test('has new issue button', async ({ page }) => { await page.goto(`/en/projects/${projectId}/issues`); await expect(page.getByRole('button', { name: /new issue/i })).toBeVisible(); }); test('has sync button', async ({ page }) => { await page.goto(`/en/projects/${projectId}/issues`); await expect(page.getByRole('button', { name: /sync/i })).toBeVisible(); }); }); test.describe('Issue Detail Page', () => { const issueId = 'ISS-001'; test('displays issue details', async ({ page }) => { await page.goto(`/en/projects/${projectId}/issues/${issueId}`); // Wait for the page to load await expect(page.getByRole('heading', { level: 1 })).toBeVisible(); // Should show issue number await expect(page.getByText(/#\d+/)).toBeVisible(); }); test('displays status badge', async ({ page }) => { await page.goto(`/en/projects/${projectId}/issues/${issueId}`); // Should show status await expect( page.getByText(/open|in progress|in review|blocked|done|closed/i).first() ).toBeVisible(); }); test('displays priority badge', async ({ page }) => { await page.goto(`/en/projects/${projectId}/issues/${issueId}`); // Should show priority await expect(page.getByText(/high|medium|low/i).first()).toBeVisible(); }); test('has back button', async ({ page }) => { await page.goto(`/en/projects/${projectId}/issues/${issueId}`); const backButton = page.getByRole('link', { name: /back to issues/i }); await expect(backButton).toBeVisible(); }); test('displays status workflow panel', async ({ page }) => { await page.goto(`/en/projects/${projectId}/issues/${issueId}`); // Should show status workflow await expect(page.getByRole('heading', { name: /status workflow/i })).toBeVisible(); // Should show all status options await expect(page.getByRole('radiogroup', { name: /issue status/i })).toBeVisible(); }); test('displays activity timeline', async ({ page }) => { await page.goto(`/en/projects/${projectId}/issues/${issueId}`); // Should show activity section await expect(page.getByRole('heading', { name: /activity/i })).toBeVisible(); // Should have add comment button await expect(page.getByRole('button', { name: /add comment/i })).toBeVisible(); }); test('displays issue details panel', async ({ page }) => { await page.goto(`/en/projects/${projectId}/issues/${issueId}`); // Should show details section await expect(page.getByRole('heading', { name: /details/i })).toBeVisible(); // Should show assignee info await expect(page.getByText(/assignee/i)).toBeVisible(); }); test('can change status via workflow', async ({ page }) => { await page.goto(`/en/projects/${projectId}/issues/${issueId}`); // Wait for page to load await page.waitForSelector('[role="radiogroup"]'); // Click on a different status const inProgressOption = page.getByRole('radio', { name: /in progress/i }); await inProgressOption.click(); // The status should update (optimistic update) await expect(inProgressOption).toHaveAttribute('aria-checked', 'true'); }); test('displays description', async ({ page }) => { await page.goto(`/en/projects/${projectId}/issues/${issueId}`); // Should show description heading await expect(page.getByRole('heading', { name: /description/i })).toBeVisible(); }); test('shows edit button', async ({ page }) => { await page.goto(`/en/projects/${projectId}/issues/${issueId}`); await expect(page.getByRole('button', { name: /edit/i })).toBeVisible(); }); test('shows external link when available', async ({ page }) => { await page.goto(`/en/projects/${projectId}/issues/${issueId}`); // The mock data includes an external URL await expect(page.getByRole('link', { name: /view in gitea/i })).toBeVisible(); }); }); test.describe('Accessibility', () => { test('issue list has proper heading structure', async ({ page }) => { await page.goto(`/en/projects/${projectId}/issues`); // Main heading should be h1 const h1 = page.getByRole('heading', { level: 1 }); await expect(h1).toBeVisible(); }); test('issue list table has proper ARIA labels', async ({ page }) => { await page.goto(`/en/projects/${projectId}/issues`); // Wait for table to load await page.waitForSelector('[data-testid^="issue-row-"]'); // Checkboxes should have labels const checkboxes = page.getByRole('checkbox'); const count = await checkboxes.count(); expect(count).toBeGreaterThan(0); // First checkbox should have accessible label await expect(checkboxes.first()).toHaveAccessibleName(/.+/); }); test('issue detail has proper radiogroup for status', async ({ page }) => { await page.goto(`/en/projects/${projectId}/issues/ISS-001`); // Status workflow should be a radiogroup const radiogroup = page.getByRole('radiogroup', { name: /issue status/i }); await expect(radiogroup).toBeVisible(); // Each status should be a radio button const radios = page.getByRole('radio'); const count = await radios.count(); expect(count).toBe(6); // 6 statuses }); test('activity timeline has proper list structure', async ({ page }) => { await page.goto(`/en/projects/${projectId}/issues/ISS-001`); // Activity should be a list const list = page.getByRole('list', { name: /issue activity/i }); await expect(list).toBeVisible(); }); }); });