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:
johba 2026-03-23 10:55:01 +00:00
parent 8d67e61c17
commit f3a2a7100f
6 changed files with 173 additions and 11 deletions

View file

@ -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();

View file

@ -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();

View 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');
}
});
});

View file

@ -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(() => {