{"slug": "end-to-end-e2e-testing-pipeline", "title": "End-to-End (E2E) testing pipeline", "summary": "The article provides a guide on building an End-to-End (E2E) testing pipeline using Playwright and GitHub Actions, explaining how to test applications from a user's perspective rather than testing individual functions. It covers setting up the project structure, writing test scripts for user interactions like login and navigation, and configuring the pipeline to run automatically on code pushes or pull requests. The guide also includes advanced features such as auto-capturing screenshots and videos on test failures, running tests against production URLs, and best practices for writing reliable tests.", "body_md": "Let’s build a real End-to-End (E2E) testing pipeline like teams use in production using Playwright (recommended) and GitHub Actions.\nI’ll show you:\nE2E (End-to-End) testing means:\n“Test your app like a real user would use it.”\nInstead of testing functions, you test:\n👉 We’ll use Playwright (industry standard in 2025)\n``` bash id=\"pw1\"\nnpm init playwright@latest\nWhen prompted choose:\n* JavaScript or TypeScript (TS recommended)\n* Tests folder: `tests`\n* GitHub Actions: YES\n---\n# 📁 2. Project structure\n``` plaintext id=\"pw2\"\nmy-app/\n├── tests/\n│ ├── example.spec.ts\n├── playwright.config.ts\n├── package.json\n``` ts id=\"test1\"\nimport { test, expect } from '@playwright/test';\ntest('user can login successfully', async ({ page }) => {\nawait page.goto('http://localhost:3000/login');\nawait page.fill('input[name=\"email\"]', 'test@example.com');\nawait page.fill('input[name=\"password\"]', 'password123');\nawait page.click('button[type=\"submit\"]');\nawait expect(page).toHaveURL(/dashboard/);\n});\n---\n# 🚀 4. Run tests locally\n``` bash id=\"run1\"\nnpx playwright test\nOpen UI mode (very useful):\n``` bash id=\"ui1\"\nnpx playwright test --ui\n---\n# 📸 5. Auto screenshots on failure\nPlaywright automatically captures:\n* screenshots\n* videos\n* traces\nEnable in config:\n``` ts id=\"cfg1\"\nuse: {\nscreenshot: 'only-on-failure',\nvideo: 'retain-on-failure',\ntrace: 'on-first-retry'\n}\n.github/workflows/e2e.yml\n``` yaml id=\"ci1\"\nname: E2E Tests (Playwright)\non:\npush:\nbranches: [ main ]\npull_request:\nbranches: [ main ]\njobs:\ntest:\nruns-on: ubuntu-latest\nsteps:\n- uses: actions/checkout@v4\n- name: Setup Node\nuses: actions/setup-node@v4\nwith:\nnode-version: 20\n- name: Install dependencies\nrun: npm install\n- name: Install Playwright browsers\nrun: npx playwright install --with-deps\n- name: Run Playwright tests\nrun: npx playwright test\n---\n# 🧠 7. What happens in CI\n``` plaintext id=\"flow1\"\nPush code\n↓\nGitHub Actions starts\n↓\nInstall dependencies\n↓\nInstall browsers (Chromium, Firefox, WebKit)\n↓\nRun E2E tests\n↓\nPass → allow merge\nFail → block PR + show report\nEnable report:\n``` ts id=\"rep1\"\nreporter: [['html'], ['list']]\nThen in CI:\n``` yaml id=\"rep2\"\n- name: Upload Playwright report\nuses: actions/upload-artifact@v4\nif: always()\nwith:\nname: playwright-report\npath: playwright-report\nInstead of localhost:\n``` ts id=\"prod1\"\nawait page.goto('https://your-app.vercel.app/login');\n👉 This turns it into **true production E2E testing**\n---\n# 🧪 10. Advanced real-world test examples\n---\n## 🟢 UI navigation test\n``` ts id=\"nav1\"\ntest('navigate to dashboard', async ({ page }) => {\nawait page.goto('/');\nawait page.click('text=Dashboard');\nawait expect(page).toHaveURL(/dashboard/);\n});\n``` ts id=\"form1\"\ntest('shows error for empty email', async ({ page }) => {\nawait page.goto('/login');\nawait page.click('button[type=\"submit\"]');\nawait expect(page.locator('.error')).toContainText('Email is required');\n});\n---\n## 🔵 API + UI combined test\n``` ts id=\"api1\"\ntest('data loads after API call', async ({ page }) => {\nawait page.goto('/dashboard');\nawait expect(page.locator('.loading')).toBeHidden();\nawait expect(page.locator('.chart')).toBeVisible();\n});\n``` yaml id=\"bp1\"\n``` ts id=\"rt1\"\nretries: 2\n---\n# ⚠️ 12. Common mistakes\n### ❌ Testing implementation instead of behavior\nBad:\n``` ts id=\"bad1\"\nexpect(component.state).toBe(true)\nGood:\n``` ts id=\"good1\"\nexpect(page).toHaveText('Welcome')\n---\n### ❌ No stable selectors\nUse:\n``` html id=\"sel1\"\ndata-testid=\"login-button\"\nThen:\n``` ts id=\"sel2\"\npage.getByTestId('login-button')\n---\n### ❌ Running E2E without CI\nAlways run in GitHub Actions\n---\n# 🧠 Final architecture\n``` plaintext id=\"final1\"\nPush / PR\n↓\nCI (unit tests)\n↓\nE2E tests (Playwright)\n↓\nBuild app\n↓\nDeploy to staging/production\n↓\nReport + screenshots stored in GitHub\nYou now have:", "url": "https://wpnews.pro/news/end-to-end-e2e-testing-pipeline", "canonical_source": "https://dev.to/kyl67899/end-to-end-e2e-testing-pipeline-dl8", "published_at": "2026-05-23 21:02:29+00:00", "updated_at": "2026-05-23 21:31:20.201784+00:00", "lang": "en", "topics": ["developer-tools", "open-source", "enterprise-software"], "entities": ["Playwright", "GitHub Actions", "JavaScript", "TypeScript"], "alternates": {"html": "https://wpnews.pro/news/end-to-end-e2e-testing-pipeline", "markdown": "https://wpnews.pro/news/end-to-end-e2e-testing-pipeline.md", "text": "https://wpnews.pro/news/end-to-end-e2e-testing-pipeline.txt", "jsonld": "https://wpnews.pro/news/end-to-end-e2e-testing-pipeline.jsonld"}}