20c19d15f4
Modern.js SSR intercepts all routes before any Express middleware, so the API proxy runs as a separate Express server on port 8080. Modern.js runs on 8081. The proxy uses curl subprocesses which go through the system HTTPS proxy (GOST) with a proper TLS fingerprint that the Aeroflot WAF accepts. Usage: node scripts/dev-server.mjs (replaces pnpm dev for full-stack) Also: remove stray e2e-angular test directory, fix env default to same-origin /api.
150 lines
4.6 KiB
TypeScript
150 lines
4.6 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
|
|
|
test.describe('US-100: Touch Navigation & Mobile Accessibility', () => {
|
|
test('should have 44px minimum touch targets on buttons', async ({ page, context }) => {
|
|
// Create a mobile context for touch support
|
|
const mobileContext = await context.browser()?.newContext({
|
|
viewport: { width: 375, height: 667 },
|
|
hasTouch: true,
|
|
isMobile: true,
|
|
});
|
|
|
|
if (!mobileContext) return;
|
|
|
|
const mobilePage = await mobileContext.newPage();
|
|
await mobilePage.goto('/ru-ru/schedule');
|
|
await mobilePage.waitForLoadState('networkidle');
|
|
|
|
const buttons = await mobilePage.locator('button').all();
|
|
|
|
for (const button of buttons) {
|
|
const box = await button.boundingBox();
|
|
if (box) {
|
|
expect(box.width).toBeGreaterThanOrEqual(44);
|
|
expect(box.height).toBeGreaterThanOrEqual(44);
|
|
}
|
|
}
|
|
|
|
await mobileContext.close();
|
|
});
|
|
|
|
test('should have proper spacing between touch targets', async ({ page, context }) => {
|
|
// Create a mobile context
|
|
const mobileContext = await context.browser()?.newContext({
|
|
viewport: { width: 375, height: 667 },
|
|
hasTouch: true,
|
|
isMobile: true,
|
|
});
|
|
|
|
if (!mobileContext) return;
|
|
|
|
const mobilePage = await mobileContext.newPage();
|
|
await mobilePage.goto('/ru-ru/schedule');
|
|
|
|
const buttons = await mobilePage.locator('button').all();
|
|
|
|
// Check if any two adjacent buttons on the same row have sufficient spacing
|
|
let foundValidSpacing = false;
|
|
for (let i = 0; i < buttons.length - 1; i++) {
|
|
const box1 = await buttons[i].boundingBox();
|
|
const box2 = await buttons[i + 1].boundingBox();
|
|
|
|
if (box1 && box2) {
|
|
// Check if buttons are roughly on the same vertical line (within 10px)
|
|
const onSameRow = Math.abs(box1.y - box2.y) < 10;
|
|
|
|
if (onSameRow) {
|
|
// Minimum 8px spacing between targets
|
|
const horizontalSpacing = box2.x - (box1.x + box1.width);
|
|
if (horizontalSpacing >= 8) {
|
|
foundValidSpacing = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// If no horizontal spacing found, buttons are likely stacked vertically which is fine
|
|
// Just verify that vertically stacked buttons have minimum size
|
|
if (buttons.length >= 2) {
|
|
const box1 = await buttons[0].boundingBox();
|
|
const box2 = await buttons[1].boundingBox();
|
|
|
|
if (box1 && box2) {
|
|
// Either they have horizontal spacing >= 8px or they're on different rows (which is valid)
|
|
const onSameRow = Math.abs(box1.y - box2.y) < 10;
|
|
const horizontalSpacing = box2.x - (box1.x + box1.width);
|
|
|
|
if (onSameRow) {
|
|
expect(horizontalSpacing).toBeGreaterThanOrEqual(8);
|
|
} else {
|
|
// Vertical stacking is valid, just ensure minimum height
|
|
expect(box1.height).toBeGreaterThanOrEqual(44);
|
|
expect(box2.height).toBeGreaterThanOrEqual(44);
|
|
}
|
|
}
|
|
}
|
|
|
|
await mobileContext.close();
|
|
});
|
|
|
|
test('should not zoom on input focus', async ({ page, context }) => {
|
|
// Create a mobile context
|
|
const mobileContext = await context.browser()?.newContext({
|
|
viewport: { width: 375, height: 667 },
|
|
hasTouch: true,
|
|
isMobile: true,
|
|
});
|
|
|
|
if (!mobileContext) return;
|
|
|
|
const mobilePage = await mobileContext.newPage();
|
|
await mobilePage.goto('/ru-ru/schedule');
|
|
|
|
// Get initial zoom level
|
|
const initialZoom = await mobilePage.evaluate(() => window.devicePixelRatio);
|
|
|
|
// Focus an input
|
|
const input = mobilePage.locator('input').first();
|
|
await input.focus();
|
|
|
|
// Check zoom level hasn't changed
|
|
const zoomAfterFocus = await mobilePage.evaluate(() => window.devicePixelRatio);
|
|
|
|
expect(zoomAfterFocus).toBe(initialZoom);
|
|
|
|
await mobileContext.close();
|
|
});
|
|
|
|
test('should console have zero errors on mobile', async ({ page, context }) => {
|
|
// Create a mobile context with touch support
|
|
const mobileContext = await context.browser()?.newContext({
|
|
viewport: { width: 375, height: 667 },
|
|
hasTouch: true,
|
|
isMobile: true,
|
|
});
|
|
|
|
if (!mobileContext) return;
|
|
|
|
const mobilePage = await mobileContext.newPage();
|
|
|
|
const errors: string[] = [];
|
|
mobilePage.on('console', (msg) => {
|
|
if (msg.type() === 'error') errors.push(msg.text());
|
|
});
|
|
|
|
await mobilePage.goto('/ru-ru/schedule');
|
|
await mobilePage.waitForLoadState('networkidle');
|
|
|
|
// Simulate touch interactions
|
|
const buttons = await mobilePage.locator('button').all();
|
|
if (buttons.length > 0) {
|
|
await buttons[0].tap();
|
|
}
|
|
|
|
expect(errors).toHaveLength(0);
|
|
|
|
await mobileContext.close();
|
|
});
|
|
});
|