End-to-End Testing
This document outlines the end-to-end (E2E) testing strategy for the BookWish platform, covering cross-platform testing for mobile, web, and API integration.
Overview
End-to-end tests verify complete user flows across the entire BookWish ecosystem:
- Flutter mobile app (iOS/Android)
- Next.js store websites
- Backend API
- Third-party integrations (Stripe, Square, Firebase)
Current Status
E2E testing is not yet implemented for the BookWish platform. This document serves as a specification for future implementation.
Testing Framework Options
For Flutter Mobile App
Option 1: Flutter Integration Tests (Recommended)
# pubspec.yaml
dev_dependencies:
integration_test:
sdk: flutter
Advantages:
- Native Flutter support
- Runs on real devices and emulators
- No additional setup required
- Direct access to Flutter widgets
Example:
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:bookwish/main.dart' as app;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('BookWish E2E Tests', () {
testWidgets('Complete book purchase flow', (tester) async {
app.main();
await tester.pumpAndSettle();
// Sign in
await tester.tap(find.text('Sign In'));
await tester.pumpAndSettle();
await tester.enterText(find.byKey(Key('email')), 'test@example.com');
await tester.enterText(find.byKey(Key('password')), 'password123');
await tester.tap(find.text('Login'));
await tester.pumpAndSettle();
// Search for book
await tester.tap(find.byIcon(Icons.search));
await tester.pumpAndSettle();
await tester.enterText(find.byType(TextField), 'Harry Potter');
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pumpAndSettle();
// Select book
await tester.tap(find.byType(BookCard).first);
await tester.pumpAndSettle();
// Add to cart
await tester.tap(find.text('Add to Cart'));
await tester.pumpAndSettle();
// Verify cart
expect(find.text('1 item'), findsOneWidget);
});
});
}
Option 2: Detox (Alternative)
For React Native-style E2E testing on Flutter:
{
"detox": {
"configurations": {
"ios.sim.debug": {
"device": "iPhone 14",
"app": "ios/build/Build/Products/Debug-iphonesimulator/BookWish.app"
}
}
}
}
For Next.js Store Websites
Option 1: Playwright (Recommended)
// tests/e2e/store-flow.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Store Website E2E', () => {
test('User can browse and add book to wishlist', async ({ page }) => {
// Navigate to store
await page.goto('https://bookwish.shop/mycoolbookstore');
// Verify store loads
await expect(page.locator('h1')).toContainText('My Cool Bookstore');
// Browse books
await page.click('text=Browse Books');
await expect(page).toHaveURL(/.*\/books/);
// Search for book
await page.fill('input[name="search"]', 'fiction');
await page.click('button:has-text("Search")');
await page.waitForLoadState('networkidle');
// Click first book
await page.click('.book-card >> nth=0');
// Add to wishlist (requires auth)
await page.click('text=Add to BookWish');
// Should redirect to login if not authenticated
await expect(page).toHaveURL(/.*\/login/);
});
test('Authenticated user can add to wishlist', async ({ page }) => {
// Login first
await page.goto('https://bookwish.io/login');
await page.fill('input[type="email"]', 'test@example.com');
await page.fill('input[type="password"]', 'password');
await page.click('button:has-text("Sign In")');
// Navigate to store
await page.goto('https://bookwish.shop/mycoolbookstore/books/123');
// Add to wishlist
await page.click('text=Add to BookWish');
// Select wishlist
await page.click('text=Reading List');
// Verify success
await expect(page.locator('text=Added to wishlist')).toBeVisible();
});
});
Option 2: Cypress (Alternative)
// cypress/e2e/store-flow.cy.ts
describe('Store E2E Tests', () => {
it('should display store homepage', () => {
cy.visit('/mycoolbookstore');
cy.contains('h1', 'My Cool Bookstore').should('be.visible');
cy.get('.featured-books').should('have.length.at.least', 1);
});
it('should search and filter books', () => {
cy.visit('/mycoolbookstore/books');
cy.get('input[name="search"]').type('science fiction');
cy.get('button[type="submit"]').click();
cy.url().should('include', 'search=science+fiction');
cy.get('.book-card').should('have.length.at.least', 1);
});
});
Critical User Flows to Test
1. Mobile App (Flutter)
Authentication Flow
testWidgets('User can sign up and sign in', (tester) async {
// Sign up
await tester.tap(find.text('Create Account'));
await tester.enterText(find.byKey(Key('email')), 'newuser@test.com');
await tester.enterText(find.byKey(Key('password')), 'SecurePass123');
await tester.tap(find.text('Sign Up'));
await tester.pumpAndSettle();
expect(find.text('Welcome'), findsOneWidget);
// Sign out
await tester.tap(find.byIcon(Icons.person));
await tester.tap(find.text('Sign Out'));
await tester.pumpAndSettle();
// Sign in
await tester.tap(find.text('Sign In'));
await tester.enterText(find.byKey(Key('email')), 'newuser@test.com');
await tester.enterText(find.byKey(Key('password')), 'SecurePass123');
await tester.tap(find.text('Login'));
await tester.pumpAndSettle();
expect(find.text('Welcome back'), findsOneWidget);
});
Book Discovery & Purchase
testWidgets('User can discover and purchase book', (tester) async {
// Search for book
await tester.tap(find.byIcon(Icons.search));
await tester.enterText(find.byType(TextField), 'The Great Gatsby');
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pumpAndSettle();
// Select book
await tester.tap(find.byType(BookCard).first);
await tester.pumpAndSettle();
// Add to cart
await tester.tap(find.text('Add to Cart'));
await tester.pumpAndSettle();
// Go to cart
await tester.tap(find.byIcon(Icons.shopping_cart));
await tester.pumpAndSettle();
// Checkout
await tester.tap(find.text('Checkout'));
await tester.pumpAndSettle();
// Fill shipping info
await tester.enterText(find.byKey(Key('address')), '123 Main St');
await tester.enterText(find.byKey(Key('city')), 'New York');
await tester.tap(find.text('Continue'));
await tester.pumpAndSettle();
// Verify payment screen
expect(find.text('Payment'), findsOneWidget);
});
Wishlist Management
testWidgets('User can create and manage wishlists', (tester) async {
// Create wishlist
await tester.tap(find.text('Wish'));
await tester.tap(find.byIcon(Icons.add));
await tester.enterText(find.byKey(Key('wishlist_name')), 'Summer Reading');
await tester.tap(find.text('Create'));
await tester.pumpAndSettle();
// Add book to wishlist
await tester.tap(find.byIcon(Icons.search));
await tester.enterText(find.byType(TextField), 'fiction');
await tester.pumpAndSettle();
await tester.tap(find.byType(BookCard).first);
await tester.tap(find.text('Add to Wishlist'));
await tester.tap(find.text('Summer Reading'));
await tester.pumpAndSettle();
expect(find.text('Added to wishlist'), findsOneWidget);
});
2. Store Website (Next.js)
Store Discovery
test('User can discover store and browse inventory', async ({ page }) => {
await page.goto('https://bookwish.shop');
// Search for store
await page.fill('input[placeholder*="search"]', 'bookstore');
await page.keyboard.press('Enter');
// Click store from results
await page.click('.store-card >> nth=0');
// Verify store page loads
await expect(page.locator('h1')).toBeVisible();
// Browse books
await page.click('text=Browse Books');
await expect(page.locator('.book-card')).toHaveCount.greaterThan(0);
});
Book Detail & Add to Wishlist
test('User can view book details and add to wishlist', async ({ page, context }) => {
// Login to BookWish first
await page.goto('https://bookwish.io/login');
await page.fill('input[type="email"]', process.env.TEST_EMAIL);
await page.fill('input[type="password"]', process.env.TEST_PASSWORD);
await page.click('button[type="submit"]');
await page.waitForNavigation();
// Visit store book page
await page.goto('https://bookwish.shop/mycoolbookstore/books/123');
// Verify book details
await expect(page.locator('h1')).toBeVisible();
await expect(page.locator('text=/\\$\\d+\\.\\d{2}/')).toBeVisible();
// Add to wishlist
await page.click('text=Add to BookWish');
await page.click('text=My Wishlist');
// Verify success
await expect(page.locator('text=Added to wishlist')).toBeVisible();
});
3. Cross-Platform Integration
Store to App Flow
test('User can navigate from store website to app', async ({ page }) => {
await page.goto('https://bookwish.shop/mycoolbookstore');
// Click "Get the App" or similar CTA
const [popup] = await Promise.all([
page.waitForEvent('popup'),
page.click('text=Get the App')
]);
// Verify app download page
await expect(popup).toHaveURL(/.*bookwish\.io/);
});
API Integration Testing
Backend API Tests
import { test, expect } from '@playwright/test';
test.describe('API Integration', () => {
test('API returns correct book data', async ({ request }) => {
const response = await request.get(
'https://api.bookwish.io/books/search?q=fiction'
);
expect(response.ok()).toBeTruthy();
const data = await response.json();
expect(data.books).toBeDefined();
expect(data.books.length).toBeGreaterThan(0);
});
test('Authenticated requests work correctly', async ({ request }) => {
const token = process.env.TEST_AUTH_TOKEN;
const response = await request.get('https://api.bookwish.io/users/me', {
headers: {
'Authorization': `Bearer ${token}`
}
});
expect(response.ok()).toBeTruthy();
const user = await response.json();
expect(user.email).toBeDefined();
});
});
Test Data Management
Test User Setup
// test-setup.ts
export const TEST_USERS = {
regular: {
email: 'test.user@bookwish.test',
password: 'TestPassword123!',
tier: 'free'
},
premium: {
email: 'premium.user@bookwish.test',
password: 'TestPassword123!',
tier: 'premium'
},
bookstore: {
email: 'bookstore.owner@bookwish.test',
password: 'TestPassword123!',
tier: 'bookstore'
}
};
export async function createTestUser(userData) {
// Create user via API
const response = await fetch('https://api.bookwish.io/auth/signup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
});
return response.json();
}
export async function cleanupTestUsers() {
// Delete test users after tests
for (const user of Object.values(TEST_USERS)) {
await fetch(`https://api.bookwish.io/users/${user.email}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${ADMIN_TOKEN}` }
});
}
}
Test Database
import { PrismaClient } from '@prisma/client';
export const testDb = new PrismaClient({
datasources: {
db: {
url: process.env.TEST_DATABASE_URL
}
}
});
export async function seedTestData() {
// Create test books
await testDb.book.createMany({
data: [
{ isbn13: '9781234567890', title: 'Test Book 1', authors: ['Author 1'] },
{ isbn13: '9780987654321', title: 'Test Book 2', authors: ['Author 2'] },
]
});
// Create test store
await testDb.store.create({
data: {
name: 'Test Bookstore',
slug: 'test-bookstore',
websiteEnabled: true
}
});
}
export async function cleanupTestData() {
await testDb.book.deleteMany({
where: { isbn13: { startsWith: '978' } }
});
await testDb.store.deleteMany({
where: { slug: 'test-bookstore' }
});
}
Running E2E Tests
Flutter Integration Tests
# Run on connected device
flutter test integration_test/
# Run on specific device
flutter test integration_test/ -d <device_id>
# Run on iOS simulator
flutter test integration_test/ -d "iPhone 14"
# Run on Android emulator
flutter test integration_test/ -d emulator-5554
Playwright Tests
# Install browsers
npx playwright install
# Run all tests
npx playwright test
# Run specific test file
npx playwright test tests/e2e/store-flow.spec.ts
# Run in headed mode (see browser)
npx playwright test --headed
# Run with UI mode
npx playwright test --ui
# Generate report
npx playwright show-report
Playwright Configuration
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests/e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'https://bookwish.shop',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
{ name: 'Mobile Chrome', use: { ...devices['Pixel 5'] } },
{ name: 'Mobile Safari', use: { ...devices['iPhone 12'] } },
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});
CI/CD Integration
GitHub Actions for E2E Tests
name: E2E Tests
on: [push, pull_request]
jobs:
flutter-e2e:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
- run: flutter pub get
- run: flutter test integration_test/
working-directory: ./app
playwright-e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm ci
working-directory: ./stores
- run: npx playwright install --with-deps
- run: npx playwright test
working-directory: ./stores
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: stores/playwright-report/
Best Practices
1. Test Isolation
- Each test should be independent
- Clean up test data after each test
- Use unique test data identifiers
2. Realistic Scenarios
- Test complete user journeys
- Include error scenarios
- Test edge cases
3. Stable Selectors
// ✅ Good - stable selectors
await page.click('[data-testid="add-to-cart"]');
await page.click('button:has-text("Checkout")');
// ❌ Bad - fragile selectors
await page.click('.btn-primary.mt-4.px-6');
await page.click('div > div > button:nth-child(3)');
4. Wait Strategies
// Wait for network to be idle
await page.waitForLoadState('networkidle');
// Wait for specific element
await page.waitForSelector('[data-testid="book-list"]');
// Wait for API response
await page.waitForResponse(response =>
response.url().includes('/api/books')
);
5. Error Handling
test('handles network errors gracefully', async ({ page }) => {
// Simulate offline
await page.route('**/*', route => route.abort());
await page.goto('/books');
// Verify error message shown
await expect(page.locator('text=Network error')).toBeVisible();
});
Future Implementation Plan
Phase 1: Foundation
- Set up Playwright for store websites
- Create basic smoke tests
- Implement test data management
Phase 2: Core Flows
- Authentication flows
- Book search and discovery
- Wishlist management
- Cart and checkout (mock payments)
Phase 3: Flutter Integration
- Set up Flutter integration tests
- Implement critical user flows
- Cross-platform authentication
Phase 4: Advanced
- Visual regression testing
- Performance monitoring
- Load testing
- Mobile-specific flows (barcode scanning, camera)
Monitoring & Reporting
Test Metrics to Track
- Test execution time
- Pass/fail rates
- Flaky test identification
- Coverage of critical paths
- Browser/device compatibility
Reporting Tools
- Playwright HTML Reporter
- Allure Reports
- TestRail integration
- Slack notifications for failures