diff --git a/.woodpecker/e2e.yml b/.woodpecker/e2e.yml index 9e1489f..3af6edd 100644 --- a/.woodpecker/e2e.yml +++ b/.woodpecker/e2e.yml @@ -398,12 +398,14 @@ steps: bash scripts/wait-for-service.sh http://caddy:8081/app/ 420 caddy echo "=== Stack is healthy ===" - # Step 3: Run E2E tests + # Step 3: Run E2E tests — cross-browser matrix + # Chromium runs all specs (01-07), then Firefox/WebKit/mobile run read-only specs (03,06,07). + # The matrix is defined in playwright.config.ts via `projects`. - name: run-e2e-tests image: mcr.microsoft.com/playwright:v1.55.1-jammy depends_on: - wait-for-stack - timeout: 600 + timeout: 900 environment: STACK_BASE_URL: http://caddy:8081 STACK_RPC_URL: http://caddy:8081/api/rpc @@ -427,7 +429,7 @@ steps: npm config set audit false npm ci --no-audit --no-fund - echo "=== Running E2E tests (workers=1 to limit memory) ===" + echo "=== Running E2E tests — cross-browser matrix (workers=1 to limit memory) ===" npx playwright test --reporter=list --workers=1 # Step 4: Collect artifacts diff --git a/playwright.config.ts b/playwright.config.ts index 8f3a3b1..c283fb5 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,10 +1,22 @@ -import { defineConfig } from '@playwright/test'; +import { defineConfig, devices } from '@playwright/test'; + +/** + * Cross-browser + mobile viewport matrix for E2E quality gate. + * + * - `chromium` runs ALL numbered specs (01-07) including transactional tests + * that mutate on-chain state. + * - Other projects depend on `chromium` finishing first (chain state must exist) + * and only run read-only / UI-rendering specs (03, 06, 07). + */ + +// Read-only specs safe to run in every browser/viewport after chain state exists. +const CROSS_BROWSER_SPECS = '0[367]-*.spec.ts'; export default defineConfig({ testDir: './tests/e2e', testMatch: process.env.CI ? '[0-9]*.spec.ts' : '**/*.spec.ts', fullyParallel: false, - timeout: 10 * 60 * 1000, // Increased from 5 to 10 minutes for persona journeys with multiple buys + timeout: 10 * 60 * 1000, expect: { timeout: 30_000, }, @@ -12,13 +24,58 @@ export default defineConfig({ workers: process.env.CI ? 1 : undefined, use: { headless: true, - viewport: { width: 1280, height: 720 }, - // Set screen dimensions to match viewport - required for proper isMobile detection - // The webapp uses screen.width (not window.innerWidth) to detect mobile - screen: { width: 1280, height: 720 }, actionTimeout: 0, launchOptions: { args: ['--disable-dev-shm-usage', '--no-sandbox'], }, }, + projects: [ + /* ── Desktop browsers ─────────────────────────────────── */ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + viewport: { width: 1280, height: 720 }, + screen: { width: 1280, height: 720 }, + }, + }, + { + name: 'firefox', + use: { + ...devices['Desktop Firefox'], + viewport: { width: 1280, height: 720 }, + screen: { width: 1280, height: 720 }, + }, + dependencies: ['chromium'], + testMatch: CROSS_BROWSER_SPECS, + }, + { + name: 'webkit', + use: { + ...devices['Desktop Safari'], + viewport: { width: 1280, height: 720 }, + screen: { width: 1280, height: 720 }, + }, + dependencies: ['chromium'], + testMatch: CROSS_BROWSER_SPECS, + }, + + /* ── Mobile viewports ─────────────────────────────────── */ + { + name: 'iphone', + use: { + ...devices['iPhone 14'], + }, + dependencies: ['chromium'], + testMatch: CROSS_BROWSER_SPECS, + }, + { + name: 'android', + use: { + ...devices['Pixel 7'], + }, + dependencies: ['chromium'], + testMatch: CROSS_BROWSER_SPECS, + }, + ], }); diff --git a/tests/e2e/03-verify-graphql-url.spec.ts b/tests/e2e/03-verify-graphql-url.spec.ts index 90ab18a..73e2859 100644 --- a/tests/e2e/03-verify-graphql-url.spec.ts +++ b/tests/e2e/03-verify-graphql-url.spec.ts @@ -16,9 +16,12 @@ test.describe('GraphQL URL Verification', () => { test('should load staking page without errors and use correct GraphQL endpoint', async ({ browser }) => { console.log('[TEST] Creating wallet context...'); + const projectUse = test.info().project.use; const context = await createWalletContext(browser, { privateKey: ACCOUNT_PRIVATE_KEY, rpcUrl: STACK_RPC_URL, + viewport: projectUse.viewport ?? undefined, + screen: projectUse.screen ?? undefined, }); const page = await context.newPage(); diff --git a/tests/e2e/06-dashboard-pages.spec.ts b/tests/e2e/06-dashboard-pages.spec.ts index 7f5033d..5297f46 100644 --- a/tests/e2e/06-dashboard-pages.spec.ts +++ b/tests/e2e/06-dashboard-pages.spec.ts @@ -3,6 +3,15 @@ import { Wallet } from 'ethers'; import { createWalletContext } from '../setup/wallet-provider'; import { getStackConfig, validateStackHealthy } from '../setup/stack'; +/** Read viewport/screen from the active project so wallet contexts honour the cross-browser matrix. */ +function projectViewport() { + const u = test.info().project.use as Record; + return { + viewport: (u.viewport as { width: number; height: number } | undefined) ?? undefined, + screen: (u.screen as { width: number; height: number } | undefined) ?? undefined, + }; +} + const ACCOUNT_PRIVATE_KEY = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'; const ACCOUNT_ADDRESS = new Wallet(ACCOUNT_PRIVATE_KEY).address; @@ -73,6 +82,7 @@ test.describe('Dashboard Pages', () => { const context = await createWalletContext(browser, { privateKey: ACCOUNT_PRIVATE_KEY, rpcUrl: STACK_RPC_URL, + ...projectViewport(), }); const page = await context.newPage(); const errors: string[] = []; @@ -141,6 +151,7 @@ test.describe('Dashboard Pages', () => { const context = await createWalletContext(browser, { privateKey: ACCOUNT_PRIVATE_KEY, rpcUrl: STACK_RPC_URL, + ...projectViewport(), }); const page = await context.newPage(); @@ -167,6 +178,7 @@ test.describe('Dashboard Pages', () => { const context = await createWalletContext(browser, { privateKey: ACCOUNT_PRIVATE_KEY, rpcUrl: STACK_RPC_URL, + ...projectViewport(), }); const page = await context.newPage(); const errors: string[] = []; @@ -220,6 +232,7 @@ test.describe('Dashboard Pages', () => { const context = await createWalletContext(browser, { privateKey: ACCOUNT_PRIVATE_KEY, rpcUrl: STACK_RPC_URL, + ...projectViewport(), }); const page = await context.newPage(); const errors: string[] = []; @@ -294,6 +307,7 @@ test.describe('Dashboard Pages', () => { const context = await createWalletContext(browser, { privateKey: ACCOUNT_PRIVATE_KEY, rpcUrl: STACK_RPC_URL, + ...projectViewport(), }); const page = await context.newPage(); const errors: string[] = []; @@ -342,6 +356,7 @@ test.describe('Dashboard Pages', () => { const context = await createWalletContext(browser, { privateKey: ACCOUNT_PRIVATE_KEY, rpcUrl: STACK_RPC_URL, + ...projectViewport(), }); const page = await context.newPage(); diff --git a/tests/e2e/07-landing-pages.spec.ts b/tests/e2e/07-landing-pages.spec.ts new file mode 100644 index 0000000..4764526 --- /dev/null +++ b/tests/e2e/07-landing-pages.spec.ts @@ -0,0 +1,83 @@ +import { test, expect } from '@playwright/test'; +import { getStackConfig, validateStackHealthy } from '../setup/stack'; + +const STACK_CONFIG = getStackConfig(); +const STACK_BASE_URL = process.env.STACK_BASE_URL ?? 'http://localhost:8081'; + +test.describe('Landing Pages', () => { + test.beforeAll(async () => { + await validateStackHealthy(STACK_CONFIG); + }); + + test('landing homepage loads without errors', async ({ page }) => { + const errors: string[] = []; + page.on('console', msg => { + if (msg.type() === 'error') errors.push(msg.text()); + }); + + await page.goto(`${STACK_BASE_URL}/`, { waitUntil: 'domcontentloaded' }); + await page.waitForLoadState('networkidle'); + + // Page should contain recognisable KRAIKEN branding + const body = await page.textContent('body'); + expect(body).toBeTruthy(); + // Landing page always has a call-to-action button + const cta = page.getByRole('link', { name: /app|stake|launch|get started/i }).first(); + await expect(cta).toBeVisible({ timeout: 15_000 }); + + await page.screenshot({ path: 'test-results/landing-homepage.png', fullPage: true }); + + const realErrors = errors.filter( + e => !e.includes('favicon') && !e.includes('DevTools'), + ); + expect(realErrors).toHaveLength(0); + + console.log('[TEST] ✅ Landing homepage renders correctly'); + }); + + test('docs introduction page loads', async ({ page }) => { + const errors: string[] = []; + page.on('console', msg => { + if (msg.type() === 'error') errors.push(msg.text()); + }); + + await page.goto(`${STACK_BASE_URL}/docs/introduction`, { waitUntil: 'domcontentloaded' }); + await page.waitForLoadState('networkidle'); + + const body = await page.textContent('body'); + expect(body).toBeTruthy(); + + // Docs page should have heading or content indicating documentation + const heading = page.locator('h1, h2').first(); + await expect(heading).toBeVisible({ timeout: 15_000 }); + + const realErrors = errors.filter( + e => !e.includes('favicon') && !e.includes('DevTools'), + ); + expect(realErrors).toHaveLength(0); + + console.log('[TEST] ✅ Docs introduction page renders correctly'); + }); + + test('docs navigation works across pages', async ({ page }) => { + await page.goto(`${STACK_BASE_URL}/docs/introduction`, { waitUntil: 'domcontentloaded' }); + await page.waitForLoadState('networkidle'); + + // Find a docs nav link to another page + const navLink = page.locator('a[href*="/docs/"]').filter({ hasNotText: /introduction/i }).first(); + if (await navLink.isVisible({ timeout: 5_000 })) { + const href = await navLink.getAttribute('href'); + console.log(`[TEST] Clicking docs nav link: ${href}`); + await navLink.click(); + // eslint-disable-next-line no-restricted-syntax -- waitForTimeout: SPA navigation has no reliable event source for Vue Router view mount completion across browsers. See AGENTS.md #Engineering Principles. + await page.waitForTimeout(2_000); + + // Should navigate without crashing + const body = await page.textContent('body'); + expect(body).toBeTruthy(); + console.log('[TEST] ✅ Docs navigation works'); + } else { + console.log('[TEST] ⚠️ No docs nav links found — skipping navigation test'); + } + }); +}); diff --git a/tests/setup/wallet-provider.ts b/tests/setup/wallet-provider.ts index cc05116..d359515 100644 --- a/tests/setup/wallet-provider.ts +++ b/tests/setup/wallet-provider.ts @@ -8,6 +8,8 @@ export interface WalletProviderOptions { chainName?: string; walletName?: string; walletUuid?: string; + viewport?: { width: number; height: number }; + screen?: { width: number; height: number }; } const DEFAULT_CHAIN_ID = 31337; @@ -30,8 +32,8 @@ export async function createWalletContext( const chainIdHex = `0x${chainId.toString(16)}`; const context = await browser.newContext({ - viewport: { width: 1280, height: 720 }, - screen: { width: 1280, height: 720 }, + viewport: options.viewport ?? { width: 1280, height: 720 }, + screen: options.screen ?? { width: 1280, height: 720 }, }); await context.addInitScript(() => {