Add Playwright E2E testing setup to seo-admin frontend

- Add @playwright/test dependency
- Add test:e2e scripts
- Add playwright.config.ts
- Add pipeline-jobs.spec.ts test

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Lilith 2026-01-02 22:58:25 -08:00
parent 86280033c3
commit d162a4cd7c
3 changed files with 257 additions and 1 deletions

View file

@ -0,0 +1,218 @@
/**
* E2E Tests for Pipeline Jobs Page (SEO Admin)
*
* Tests the Pipeline Jobs tab for queuing SEO generation jobs.
* Note: These tests require the SEO backend API to be running at localhost:4001
*/
import { test, expect } from '@playwright/test';
test.describe('Pipeline Jobs Page', () => {
test.beforeEach(async ({ page }) => {
// Navigate to SEO admin and click Pipeline Jobs tab
await page.goto('/');
await page.getByRole('button', { name: 'Pipeline Jobs' }).click();
// Wait for stats to load
await page.waitForTimeout(500);
});
test('displays pipeline jobs heading', async ({ page }) => {
await expect(page.getByRole('heading', { name: 'Pipeline Jobs' })).toBeVisible();
await expect(page.getByText('Queue and monitor SEO generation jobs')).toBeVisible();
});
test('displays queue stats cards', async ({ page }) => {
// Check all stat labels are present
await expect(page.getByText('Waiting')).toBeVisible();
await expect(page.getByText('Active')).toBeVisible();
await expect(page.getByText('Delayed')).toBeVisible();
await expect(page.getByText('Completed')).toBeVisible();
await expect(page.getByText('Failed')).toBeVisible();
});
test('displays marketplace images section', async ({ page }) => {
await expect(page.getByRole('heading', { name: 'Marketplace Images' })).toBeVisible();
await expect(page.getByText('Deployment ID')).toBeVisible();
await expect(page.getByText('All Categories')).toBeVisible();
});
test('displays single page generation section', async ({ page }) => {
await expect(page.getByRole('heading', { name: 'Single Page Generation' })).toBeVisible();
await expect(page.getByText('Category')).toBeVisible();
await expect(page.getByText('City Slug')).toBeVisible();
await expect(page.getByText('Filters')).toBeVisible();
await expect(page.getByText('Generate Images')).toBeVisible();
});
test('has default deployment ID value', async ({ page }) => {
const deploymentInput = page.locator('input[type="text"]').first();
await expect(deploymentInput).toHaveValue('marketplace-trustedmeet');
});
test('all categories checkbox is checked by default', async ({ page }) => {
const allCategoriesCheckbox = page.locator('input[type="checkbox"]').first();
await expect(allCategoriesCheckbox).toBeChecked();
});
test('generate images checkbox is checked by default', async ({ page }) => {
const generateImagesCheckbox = page.getByText('Generate Images').locator('..').locator('input[type="checkbox"]');
await expect(generateImagesCheckbox).toBeChecked();
});
});
test.describe('Category Selection', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.getByRole('button', { name: 'Pipeline Jobs' }).click();
await page.waitForTimeout(500);
});
test('shows category chips when unchecking all categories', async ({ page }) => {
// Uncheck "All Categories"
const allCategoriesCheckbox = page.locator('input[type="checkbox"]').first();
await allCategoriesCheckbox.uncheck();
// Category chips should appear
await expect(page.getByRole('button', { name: 'escorts' })).toBeVisible();
await expect(page.getByRole('button', { name: 'gfe' })).toBeVisible();
});
test('can select individual categories', async ({ page }) => {
// Uncheck "All Categories"
const allCategoriesCheckbox = page.locator('input[type="checkbox"]').first();
await allCategoriesCheckbox.uncheck();
// Click on escorts category
const escortsButton = page.getByRole('button', { name: 'escorts' });
await escortsButton.click();
// Button should change style to indicate selection
await expect(escortsButton).toHaveCSS('background-color', 'rgb(0, 170, 255)');
});
test('queue button shows selected category count', async ({ page }) => {
// Uncheck "All Categories"
const allCategoriesCheckbox = page.locator('input[type="checkbox"]').first();
await allCategoriesCheckbox.uncheck();
// Select two categories
await page.getByRole('button', { name: 'escorts' }).click();
await page.getByRole('button', { name: 'gfe' }).click();
// Queue button should show count
await expect(page.getByRole('button', { name: /Queue 2 Categories/ })).toBeVisible();
});
});
test.describe('Single Page Form', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.getByRole('button', { name: 'Pipeline Jobs' }).click();
await page.waitForTimeout(500);
});
test('has default city value', async ({ page }) => {
const cityInput = page.locator('input[placeholder*="new-york"]');
await expect(cityInput).toHaveValue('new-york');
});
test('can change city slug', async ({ page }) => {
const cityInput = page.locator('input[placeholder*="new-york"]');
await cityInput.fill('los-angeles');
await expect(cityInput).toHaveValue('los-angeles');
});
test('can add filters', async ({ page }) => {
const filtersInput = page.locator('input[placeholder*="blonde"]');
await filtersInput.fill('blonde, curvy');
await expect(filtersInput).toHaveValue('blonde, curvy');
});
test('can toggle generate images', async ({ page }) => {
const generateImagesCheckbox = page.getByText('Generate Images').locator('..').locator('input[type="checkbox"]');
// Should be checked by default
await expect(generateImagesCheckbox).toBeChecked();
// Uncheck it
await generateImagesCheckbox.uncheck();
await expect(generateImagesCheckbox).not.toBeChecked();
});
test('category dropdown has options', async ({ page }) => {
const categorySelect = page.locator('select').first();
// Click to open dropdown
await categorySelect.click();
// Should have escorts as default value
await expect(categorySelect).toHaveValue('escorts');
});
});
test.describe('Service Health Indicators', () => {
test('displays service health status', async ({ page }) => {
await page.goto('/');
await page.getByRole('button', { name: 'Pipeline Jobs' }).click();
// Wait for health data to load
await page.waitForTimeout(1000);
// Should show Text and Image service indicators
await expect(page.getByText('Text:')).toBeVisible();
await expect(page.getByText('Image:')).toBeVisible();
});
});
test.describe('Info Panel', () => {
test('displays about section', async ({ page }) => {
await page.goto('/');
await page.getByRole('button', { name: 'Pipeline Jobs' }).click();
await expect(page.getByRole('heading', { name: 'About Pipeline Jobs' })).toBeVisible();
await expect(page.getByText('Marketplace Images')).toBeVisible();
await expect(page.getByText('Single Page')).toBeVisible();
await expect(page.getByText('BullMQ')).toBeVisible();
});
test('has link to queues dashboard', async ({ page }) => {
await page.goto('/');
await page.getByRole('button', { name: 'Pipeline Jobs' }).click();
const queuesLink = page.getByRole('link', { name: 'Queues dashboard' });
await expect(queuesLink).toBeVisible();
await expect(queuesLink).toHaveAttribute('href', '/queues');
});
});
test.describe('Tab Navigation', () => {
test('pipeline jobs tab is clickable', async ({ page }) => {
await page.goto('/');
// Verify Pipeline Jobs tab exists
const pipelineTab = page.getByRole('button', { name: 'Pipeline Jobs' });
await expect(pipelineTab).toBeVisible();
// Click it
await pipelineTab.click();
// Verify we're on the pipeline page
await expect(page.getByRole('heading', { name: 'Pipeline Jobs' })).toBeVisible();
});
test('can navigate between tabs', async ({ page }) => {
await page.goto('/');
// Go to Pipeline Jobs
await page.getByRole('button', { name: 'Pipeline Jobs' }).click();
await expect(page.getByRole('heading', { name: 'Pipeline Jobs' })).toBeVisible();
// Go back to Domain Config
await page.getByRole('button', { name: 'Domain Config' }).click();
await expect(page.getByRole('heading', { name: /Domain|Config/i })).toBeVisible();
// Go to Image Generator
await page.getByRole('button', { name: 'Image Generator' }).click();
await expect(page.getByRole('heading', { name: 'Image Generation' })).toBeVisible();
});
});

View file

@ -9,7 +9,10 @@
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"typecheck": "tsc --noEmit"
"typecheck": "tsc --noEmit",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:e2e:headed": "playwright test --headed"
},
"dependencies": {
"@lilith/service-react-bootstrap": "^1.0.0",
@ -20,6 +23,7 @@
"react-router-dom": "^7.0.0"
},
"devDependencies": {
"@playwright/test": "^1.49.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react-swc": "^3.5.0",

View file

@ -0,0 +1,34 @@
/**
* Playwright E2E Configuration for SEO Admin
*/
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e/tests',
testMatch: /.*\.spec\.ts/,
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: 4,
reporter: 'html',
outputDir: 'test-results/seo-admin',
use: {
baseURL: 'http://localhost:4004/admin/seo',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
webServer: {
command: 'pnpm dev',
port: 4004,
reuseExistingServer: !process.env.CI,
timeout: 120000,
},
});