fix: feat: E2E quality gate — mobile viewports + cross-browser matrix (#1099)
Add Playwright projects for Chromium, Firefox, WebKit, iPhone 14, and Pixel 7 viewports. Chromium runs all specs (01-07); other projects run read-only specs (03, 06, 07) after Chromium finishes, using project dependencies to ensure chain state exists. Coverage audit: - Tests 01/02 already cover /app/get-krk, /app/cheats as part of flows - Test 03 verifies GraphQL endpoints - Test 06 covers wallet + position dashboards - New test 07 adds landing page and docs smoke coverage Changes: - playwright.config.ts: 5 projects (3 desktop browsers + 2 mobile) - wallet-provider.ts: accept optional viewport/screen for mobile contexts - 03, 06 specs: pass project viewport to wallet context - 07-landing-pages.spec.ts: new spec for landing homepage + docs - e2e.yml: timeout 600→900s for cross-browser matrix, updated comments Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8d67e61c17
commit
f3a2a7100f
6 changed files with 173 additions and 11 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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<string, unknown>;
|
||||
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();
|
||||
|
||||
|
|
|
|||
83
tests/e2e/07-landing-pages.spec.ts
Normal file
83
tests/e2e/07-landing-pages.spec.ts
Normal file
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -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(() => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue