Some checks failed
CI Pipeline / Lint Code (push) Has been cancelled
CI Pipeline / Test API Package (push) Has been cancelled
CI Pipeline / Test Web Package (push) Has been cancelled
CI Pipeline / Test Shared Package (push) Has been cancelled
CI Pipeline / Build All Packages (push) Has been cancelled
CI Pipeline / Generate Coverage Report (push) Has been cancelled
Docker Build & Deploy / Build Docker Images (push) Has been cancelled
Docker Build & Deploy / Push Docker Images (push) Has been cancelled
Docker Build & Deploy / Deploy to Staging (push) Has been cancelled
Docker Build & Deploy / Deploy to Production (push) Has been cancelled
E2E Tests / End-to-End Tests (push) Has been cancelled
E2E Tests / E2E Tests (Mobile) (push) Has been cancelled
Security Scanning / NPM Audit (push) Has been cancelled
Security Scanning / Dependency License Check (push) Has been cancelled
Security Scanning / Code Quality Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
- Add Vitest for unit testing across all packages - Add Playwright for E2E testing - Add sample tests for API, Web, and Shared packages - Configure Gitea Actions CI/CD workflows (ci, e2e, security, docker) - Add testing documentation (TESTING.md) - Add Gitea Actions setup guide - Update .gitignore for test artifacts - Add test environment configuration
242 lines
6.4 KiB
TypeScript
242 lines
6.4 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
|
|
|
test.describe('Recipe Management E2E Tests', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
// Navigate to the app before each test
|
|
await page.goto('/');
|
|
});
|
|
|
|
test('should display the recipe list page', async ({ page }) => {
|
|
// Wait for the page to load
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// Check that we're on the recipe list page
|
|
await expect(page.locator('h2')).toContainText('My Recipes');
|
|
});
|
|
|
|
test('should show empty state when no recipes exist', async ({ page }) => {
|
|
// Mock API to return empty recipe list
|
|
await page.route('**/api/recipes', async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
data: [],
|
|
total: 0,
|
|
page: 1,
|
|
pageSize: 20,
|
|
}),
|
|
});
|
|
});
|
|
|
|
await page.goto('/');
|
|
|
|
// Check for empty state message
|
|
await expect(page.getByText(/No recipes yet/i)).toBeVisible();
|
|
});
|
|
|
|
test('should display recipes when available', async ({ page }) => {
|
|
// Mock API to return recipes
|
|
await page.route('**/api/recipes', async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
data: [
|
|
{
|
|
id: '1',
|
|
title: 'Spaghetti Carbonara',
|
|
description: 'Classic Italian pasta',
|
|
totalTime: 30,
|
|
servings: 4,
|
|
},
|
|
{
|
|
id: '2',
|
|
title: 'Chocolate Cake',
|
|
description: 'Rich chocolate dessert',
|
|
totalTime: 60,
|
|
servings: 8,
|
|
},
|
|
],
|
|
total: 2,
|
|
page: 1,
|
|
pageSize: 20,
|
|
}),
|
|
});
|
|
});
|
|
|
|
await page.goto('/');
|
|
|
|
// Check that recipes are displayed
|
|
await expect(page.getByText('Spaghetti Carbonara')).toBeVisible();
|
|
await expect(page.getByText('Chocolate Cake')).toBeVisible();
|
|
});
|
|
|
|
test('should navigate to recipe detail when clicking a recipe', async ({ page }) => {
|
|
// Mock recipes list
|
|
await page.route('**/api/recipes', async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
data: [
|
|
{
|
|
id: '1',
|
|
title: 'Test Recipe',
|
|
description: 'Test Description',
|
|
},
|
|
],
|
|
total: 1,
|
|
page: 1,
|
|
pageSize: 20,
|
|
}),
|
|
});
|
|
});
|
|
|
|
// Mock recipe detail
|
|
await page.route('**/api/recipes/1', async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
data: {
|
|
id: '1',
|
|
title: 'Test Recipe',
|
|
description: 'Detailed description',
|
|
ingredients: [
|
|
{ id: '1', name: 'Flour', amount: '2', unit: 'cups', order: 0 },
|
|
],
|
|
instructions: [
|
|
{ id: '1', step: 1, text: 'Mix ingredients' },
|
|
],
|
|
},
|
|
}),
|
|
});
|
|
});
|
|
|
|
await page.goto('/');
|
|
|
|
// Click on the recipe
|
|
await page.getByText('Test Recipe').click();
|
|
|
|
// Verify we navigated to the detail page
|
|
await expect(page).toHaveURL(/\/recipes\/1/);
|
|
});
|
|
|
|
test('should import recipe from URL', async ({ page }) => {
|
|
// Mock import endpoint
|
|
await page.route('**/api/recipes/import', async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
success: true,
|
|
recipe: {
|
|
title: 'Imported Recipe',
|
|
description: 'Recipe from URL',
|
|
ingredients: [],
|
|
instructions: [],
|
|
},
|
|
}),
|
|
});
|
|
});
|
|
|
|
// Navigate to import page (adjust based on your routing)
|
|
await page.goto('/import');
|
|
|
|
// Fill in URL
|
|
await page.fill('input[type="url"]', 'https://example.com/recipe');
|
|
|
|
// Click import button
|
|
await page.click('button:has-text("Import")');
|
|
|
|
// Wait for success message or redirect
|
|
await expect(page.getByText(/Imported Recipe|Success/i)).toBeVisible({
|
|
timeout: 5000,
|
|
});
|
|
});
|
|
|
|
test('should handle API errors gracefully', async ({ page }) => {
|
|
// Mock API to return error
|
|
await page.route('**/api/recipes', async (route) => {
|
|
await route.fulfill({
|
|
status: 500,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
error: 'Internal server error',
|
|
}),
|
|
});
|
|
});
|
|
|
|
await page.goto('/');
|
|
|
|
// Check for error message
|
|
await expect(page.getByText(/Failed to load recipes|error/i)).toBeVisible();
|
|
});
|
|
|
|
test('should be responsive on mobile devices', async ({ page }) => {
|
|
// Set mobile viewport
|
|
await page.setViewportSize({ width: 375, height: 667 });
|
|
|
|
// Mock API response
|
|
await page.route('**/api/recipes', async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
data: [
|
|
{
|
|
id: '1',
|
|
title: 'Mobile Recipe',
|
|
},
|
|
],
|
|
total: 1,
|
|
page: 1,
|
|
pageSize: 20,
|
|
}),
|
|
});
|
|
});
|
|
|
|
await page.goto('/');
|
|
|
|
// Verify content is visible on mobile
|
|
await expect(page.getByText('Mobile Recipe')).toBeVisible();
|
|
});
|
|
});
|
|
|
|
test.describe('Recipe Search and Filter', () => {
|
|
test('should filter recipes by search term', async ({ page }) => {
|
|
// This test assumes there's a search functionality
|
|
await page.goto('/');
|
|
|
|
// Wait for search input to be available
|
|
const searchInput = page.locator('input[type="search"], input[placeholder*="Search"]');
|
|
|
|
if (await searchInput.count() > 0) {
|
|
await searchInput.fill('pasta');
|
|
|
|
// Mock filtered results
|
|
await page.route('**/api/recipes?*search=pasta*', async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
data: [
|
|
{
|
|
id: '1',
|
|
title: 'Pasta Carbonara',
|
|
},
|
|
],
|
|
total: 1,
|
|
page: 1,
|
|
pageSize: 20,
|
|
}),
|
|
});
|
|
});
|
|
|
|
// Verify filtered results
|
|
await expect(page.getByText('Pasta Carbonara')).toBeVisible();
|
|
}
|
|
});
|
|
});
|