mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-05 06:59:42 +02:00
chore: bump version to 0.1478
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,530 +0,0 @@
|
||||
/**
|
||||
* Comprehensive Mobile E2E Tests
|
||||
* Tests mobile-specific UI features, interactions, and usability
|
||||
*
|
||||
* Port: 3192 (unique for this test file)
|
||||
*/
|
||||
|
||||
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
|
||||
import {
|
||||
createServerFixture,
|
||||
destroyServerFixture,
|
||||
createMobileSafariFixture,
|
||||
destroyMobileBrowserFixture,
|
||||
hasBodyClass,
|
||||
getMinHeight,
|
||||
tap,
|
||||
getComputedStyle,
|
||||
CleanupTracker,
|
||||
type ServerFixture,
|
||||
type MobileBrowserFixture,
|
||||
} from '../fixtures/index.js';
|
||||
import { E2E_TIMEOUTS, MOBILE_VIEWPORTS, generateCaseName, E2E_PORTS } from '../e2e.config.js';
|
||||
|
||||
const PORT = E2E_PORTS.MOBILE_COMPREHENSIVE;
|
||||
let serverFixture: ServerFixture | null = null;
|
||||
let cleanup: CleanupTracker;
|
||||
|
||||
describe('Mobile Comprehensive E2E', () => {
|
||||
beforeAll(async () => {
|
||||
serverFixture = await createServerFixture(PORT);
|
||||
cleanup = new CleanupTracker(serverFixture.baseUrl);
|
||||
}, E2E_TIMEOUTS.BROWSER_SETUP);
|
||||
|
||||
afterAll(async () => {
|
||||
if (cleanup) {
|
||||
await cleanup.forceCleanupAll();
|
||||
}
|
||||
if (serverFixture) {
|
||||
await destroyServerFixture(serverFixture);
|
||||
}
|
||||
}, E2E_TIMEOUTS.TEST);
|
||||
|
||||
// ============================================================================
|
||||
// Welcome Overlay Tests
|
||||
// ============================================================================
|
||||
|
||||
it('should show welcome overlay with touch-friendly buttons on mobile', async () => {
|
||||
let browser: MobileBrowserFixture | null = null;
|
||||
|
||||
try {
|
||||
browser = await createMobileSafariFixture(MOBILE_VIEWPORTS.IPHONE_17_PRO);
|
||||
const { page } = browser;
|
||||
|
||||
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Welcome overlay should be visible
|
||||
const overlayVisible = await page.isVisible('.welcome-overlay');
|
||||
expect(overlayVisible).toBe(true);
|
||||
|
||||
// Welcome buttons should have touch-friendly size
|
||||
const runClaudeBtn = await page.$('.welcome-btn-primary');
|
||||
expect(runClaudeBtn).not.toBeNull();
|
||||
|
||||
const box = await runClaudeBtn!.boundingBox();
|
||||
expect(box).not.toBeNull();
|
||||
// Touch target should be at least 44px tall
|
||||
expect(box!.height).toBeGreaterThanOrEqual(44);
|
||||
|
||||
} finally {
|
||||
if (browser) {
|
||||
await destroyMobileBrowserFixture(browser);
|
||||
}
|
||||
}
|
||||
}, E2E_TIMEOUTS.TEST);
|
||||
|
||||
// ============================================================================
|
||||
// Toolbar Tests
|
||||
// ============================================================================
|
||||
|
||||
it('should show Run Claude button prominently on mobile toolbar', async () => {
|
||||
let browser: MobileBrowserFixture | null = null;
|
||||
|
||||
try {
|
||||
browser = await createMobileSafariFixture(MOBILE_VIEWPORTS.IPHONE_17_PRO);
|
||||
const { page } = browser;
|
||||
|
||||
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Run Claude button should be visible
|
||||
const runBtnVisible = await page.isVisible('.btn-toolbar.btn-claude');
|
||||
expect(runBtnVisible).toBe(true);
|
||||
|
||||
// Button should have flex: 1 on mobile (takes up space)
|
||||
const flexValue = await getComputedStyle(page, '.btn-toolbar.btn-claude', 'flex');
|
||||
// flex: 1 computes to "1 1 0%" or similar
|
||||
expect(flexValue).toContain('1');
|
||||
|
||||
} finally {
|
||||
if (browser) {
|
||||
await destroyMobileBrowserFixture(browser);
|
||||
}
|
||||
}
|
||||
}, E2E_TIMEOUTS.TEST);
|
||||
|
||||
it('should show case dropdown on mobile toolbar', async () => {
|
||||
let browser: MobileBrowserFixture | null = null;
|
||||
|
||||
try {
|
||||
browser = await createMobileSafariFixture(MOBILE_VIEWPORTS.IPHONE_17_PRO);
|
||||
const { page } = browser;
|
||||
|
||||
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Case dropdown should be visible
|
||||
const dropdownVisible = await page.isVisible('#quickStartCase');
|
||||
expect(dropdownVisible).toBe(true);
|
||||
|
||||
// Should be able to interact with it
|
||||
const options = await page.$$eval('#quickStartCase option', (opts) =>
|
||||
opts.map((o) => (o as HTMLOptionElement).value)
|
||||
);
|
||||
expect(options.length).toBeGreaterThan(0);
|
||||
|
||||
} finally {
|
||||
if (browser) {
|
||||
await destroyMobileBrowserFixture(browser);
|
||||
}
|
||||
}
|
||||
}, E2E_TIMEOUTS.TEST);
|
||||
|
||||
it('should hide version display on mobile', async () => {
|
||||
let browser: MobileBrowserFixture | null = null;
|
||||
|
||||
try {
|
||||
browser = await createMobileSafariFixture(MOBILE_VIEWPORTS.IPHONE_17_PRO);
|
||||
const { page } = browser;
|
||||
|
||||
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Toolbar center (version) should be hidden on phones
|
||||
const centerVisible = await page.isVisible('.toolbar-center');
|
||||
expect(centerVisible).toBe(false);
|
||||
|
||||
} finally {
|
||||
if (browser) {
|
||||
await destroyMobileBrowserFixture(browser);
|
||||
}
|
||||
}
|
||||
}, E2E_TIMEOUTS.TEST);
|
||||
|
||||
// ============================================================================
|
||||
// Session Tab Tests
|
||||
// ============================================================================
|
||||
|
||||
it('should allow horizontal scrolling of session tabs on mobile', async () => {
|
||||
let browser: MobileBrowserFixture | null = null;
|
||||
const caseName = generateCaseName('mobile-scroll');
|
||||
|
||||
try {
|
||||
browser = await createMobileSafariFixture(MOBILE_VIEWPORTS.IPHONE_17_PRO);
|
||||
const { page } = browser;
|
||||
cleanup.trackCase(caseName);
|
||||
|
||||
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Create case
|
||||
await fetch(`${serverFixture!.baseUrl}/api/cases`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: caseName }),
|
||||
});
|
||||
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(500);
|
||||
await page.selectOption('#quickStartCase', caseName);
|
||||
|
||||
// Create multiple sessions to enable scrolling
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await tap(page, '.btn-toolbar.btn-claude');
|
||||
await page.waitForSelector('.session-tab', { timeout: E2E_TIMEOUTS.SESSION_CREATE });
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
// Check that session tabs container allows horizontal scroll
|
||||
const overflowX = await getComputedStyle(page, '.session-tabs', 'overflow-x');
|
||||
expect(overflowX).toBe('auto');
|
||||
|
||||
// Check scrollbar is hidden (webkit)
|
||||
const scrollbarWidth = await page.$eval('.session-tabs', (el) => {
|
||||
const style = window.getComputedStyle(el, '::-webkit-scrollbar');
|
||||
return style.display;
|
||||
});
|
||||
// Should be 'none' due to scrollbar-width: none
|
||||
|
||||
// Track sessions for cleanup
|
||||
const response = await fetch(`${serverFixture!.baseUrl}/api/sessions`);
|
||||
const data = await response.json();
|
||||
data?.forEach((s: { id: string }) => cleanup.trackSession(s.id));
|
||||
|
||||
} finally {
|
||||
if (browser) {
|
||||
await destroyMobileBrowserFixture(browser);
|
||||
}
|
||||
}
|
||||
}, E2E_TIMEOUTS.TEST);
|
||||
|
||||
it('should switch between session tabs on tap', async () => {
|
||||
let browser: MobileBrowserFixture | null = null;
|
||||
const caseName = generateCaseName('mobile-switch');
|
||||
|
||||
try {
|
||||
browser = await createMobileSafariFixture(MOBILE_VIEWPORTS.IPHONE_17_PRO);
|
||||
const { page } = browser;
|
||||
cleanup.trackCase(caseName);
|
||||
|
||||
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Create case
|
||||
await fetch(`${serverFixture!.baseUrl}/api/cases`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: caseName }),
|
||||
});
|
||||
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(500);
|
||||
await page.selectOption('#quickStartCase', caseName);
|
||||
|
||||
// Create 2 sessions
|
||||
await tap(page, '.btn-toolbar.btn-claude');
|
||||
await page.waitForSelector('.session-tab.active', { timeout: E2E_TIMEOUTS.SESSION_CREATE });
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await tap(page, '.btn-toolbar.btn-claude');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Get the tabs - should have at least 2
|
||||
const tabs = await page.$$('.session-tab');
|
||||
expect(tabs.length).toBeGreaterThanOrEqual(2);
|
||||
|
||||
// Tap the first tab
|
||||
await tap(page, '.session-tab:first-child');
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// First tab should now be active
|
||||
const firstTabActive = await page.$eval('.session-tab:first-child', (el) =>
|
||||
el.classList.contains('active')
|
||||
);
|
||||
expect(firstTabActive).toBe(true);
|
||||
|
||||
// Track sessions for cleanup
|
||||
const response = await fetch(`${serverFixture!.baseUrl}/api/sessions`);
|
||||
const data = await response.json();
|
||||
data?.forEach((s: { id: string }) => cleanup.trackSession(s.id));
|
||||
|
||||
} finally {
|
||||
if (browser) {
|
||||
await destroyMobileBrowserFixture(browser);
|
||||
}
|
||||
}
|
||||
}, E2E_TIMEOUTS.TEST);
|
||||
|
||||
// ============================================================================
|
||||
// Modal Tests
|
||||
// ============================================================================
|
||||
|
||||
it('should show full-screen modals on mobile', async () => {
|
||||
let browser: MobileBrowserFixture | null = null;
|
||||
|
||||
try {
|
||||
browser = await createMobileSafariFixture(MOBILE_VIEWPORTS.IPHONE_17_PRO);
|
||||
const { page } = browser;
|
||||
|
||||
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Open app settings modal
|
||||
await tap(page, '.btn-settings');
|
||||
await page.waitForSelector('#appSettingsModal.open, #appSettingsModal:not(.hidden)', { timeout: 5000 });
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// Modal content should be full-width on mobile
|
||||
const modalWidth = await page.$eval('#appSettingsModal .modal-content', (el) =>
|
||||
el.getBoundingClientRect().width
|
||||
);
|
||||
const viewportWidth = page.viewportSize()!.width;
|
||||
|
||||
// Modal should be full width (or very close to it)
|
||||
expect(modalWidth).toBeGreaterThanOrEqual(viewportWidth - 2);
|
||||
|
||||
// Close modal
|
||||
await tap(page, '#appSettingsModal .modal-close');
|
||||
|
||||
} finally {
|
||||
if (browser) {
|
||||
await destroyMobileBrowserFixture(browser);
|
||||
}
|
||||
}
|
||||
}, E2E_TIMEOUTS.TEST);
|
||||
|
||||
it('should show create case modal on mobile', async () => {
|
||||
let browser: MobileBrowserFixture | null = null;
|
||||
|
||||
try {
|
||||
browser = await createMobileSafariFixture(MOBILE_VIEWPORTS.IPHONE_17_PRO);
|
||||
const { page } = browser;
|
||||
|
||||
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Tap the + button to create case
|
||||
await tap(page, '.btn-case-add');
|
||||
await page.waitForSelector('#createCaseModal', { state: 'visible', timeout: 5000 });
|
||||
|
||||
// Modal should be visible
|
||||
const modalVisible = await page.isVisible('#createCaseModal');
|
||||
expect(modalVisible).toBe(true);
|
||||
|
||||
// Input should be focusable
|
||||
const nameInput = await page.$('#newCaseName');
|
||||
expect(nameInput).not.toBeNull();
|
||||
|
||||
// Close modal
|
||||
await tap(page, '#createCaseModal .modal-close');
|
||||
|
||||
} finally {
|
||||
if (browser) {
|
||||
await destroyMobileBrowserFixture(browser);
|
||||
}
|
||||
}
|
||||
}, E2E_TIMEOUTS.TEST);
|
||||
|
||||
// ============================================================================
|
||||
// Header Tests
|
||||
// ============================================================================
|
||||
|
||||
it('should show notification button on mobile', async () => {
|
||||
let browser: MobileBrowserFixture | null = null;
|
||||
|
||||
try {
|
||||
browser = await createMobileSafariFixture(MOBILE_VIEWPORTS.IPHONE_17_PRO);
|
||||
const { page } = browser;
|
||||
|
||||
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Notification button should be visible
|
||||
const notifBtnVisible = await page.isVisible('.btn-notifications');
|
||||
expect(notifBtnVisible).toBe(true);
|
||||
|
||||
// Should be tappable
|
||||
await tap(page, '.btn-notifications');
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// Notification drawer should open
|
||||
const drawerVisible = await page.isVisible('#notifDrawer');
|
||||
expect(drawerVisible).toBe(true);
|
||||
|
||||
// Close drawer
|
||||
await tap(page, '.btn-notifications');
|
||||
|
||||
} finally {
|
||||
if (browser) {
|
||||
await destroyMobileBrowserFixture(browser);
|
||||
}
|
||||
}
|
||||
}, E2E_TIMEOUTS.TEST);
|
||||
|
||||
it('should show settings button on mobile', async () => {
|
||||
let browser: MobileBrowserFixture | null = null;
|
||||
|
||||
try {
|
||||
browser = await createMobileSafariFixture(MOBILE_VIEWPORTS.IPHONE_17_PRO);
|
||||
const { page } = browser;
|
||||
|
||||
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Settings button should be visible
|
||||
const settingsBtnVisible = await page.isVisible('.btn-settings');
|
||||
expect(settingsBtnVisible).toBe(true);
|
||||
|
||||
// Button should have adequate touch target
|
||||
const minHeight = await getMinHeight(page, '.btn-settings');
|
||||
expect(minHeight).toBeGreaterThanOrEqual(44);
|
||||
|
||||
} finally {
|
||||
if (browser) {
|
||||
await destroyMobileBrowserFixture(browser);
|
||||
}
|
||||
}
|
||||
}, E2E_TIMEOUTS.TEST);
|
||||
|
||||
// ============================================================================
|
||||
// Close Session Tests
|
||||
// ============================================================================
|
||||
|
||||
it('should show close confirmation modal on tap', async () => {
|
||||
let browser: MobileBrowserFixture | null = null;
|
||||
const caseName = generateCaseName('mobile-close');
|
||||
|
||||
try {
|
||||
browser = await createMobileSafariFixture(MOBILE_VIEWPORTS.IPHONE_17_PRO);
|
||||
const { page } = browser;
|
||||
cleanup.trackCase(caseName);
|
||||
|
||||
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Create case and session
|
||||
await fetch(`${serverFixture!.baseUrl}/api/cases`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: caseName }),
|
||||
});
|
||||
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(500);
|
||||
await page.selectOption('#quickStartCase', caseName);
|
||||
await tap(page, '.btn-toolbar.btn-claude');
|
||||
await page.waitForSelector('.session-tab.active', { timeout: E2E_TIMEOUTS.SESSION_CREATE });
|
||||
|
||||
// Tap the close button on the tab
|
||||
await tap(page, '.session-tab .tab-close');
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// Close confirmation modal should appear
|
||||
const modalVisible = await page.isVisible('#closeConfirmModal');
|
||||
expect(modalVisible).toBe(true);
|
||||
|
||||
// Cancel to keep session
|
||||
await tap(page, '#closeConfirmModal .modal-footer-cancel button');
|
||||
|
||||
// Track session for cleanup
|
||||
const response = await fetch(`${serverFixture!.baseUrl}/api/sessions`);
|
||||
const data = await response.json();
|
||||
data?.forEach((s: { id: string }) => cleanup.trackSession(s.id));
|
||||
|
||||
} finally {
|
||||
if (browser) {
|
||||
await destroyMobileBrowserFixture(browser);
|
||||
}
|
||||
}
|
||||
}, E2E_TIMEOUTS.TEST);
|
||||
|
||||
// ============================================================================
|
||||
// Token Display Tests
|
||||
// ============================================================================
|
||||
|
||||
it('should show token count on mobile header', async () => {
|
||||
let browser: MobileBrowserFixture | null = null;
|
||||
|
||||
try {
|
||||
browser = await createMobileSafariFixture(MOBILE_VIEWPORTS.IPHONE_17_PRO);
|
||||
const { page } = browser;
|
||||
|
||||
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Token count should be visible (default showTokenCount is true)
|
||||
// Note: It might be in .header-tokens
|
||||
const tokenDisplayExists = await page.$('.header-tokens');
|
||||
expect(tokenDisplayExists).not.toBeNull();
|
||||
|
||||
} finally {
|
||||
if (browser) {
|
||||
await destroyMobileBrowserFixture(browser);
|
||||
}
|
||||
}
|
||||
}, E2E_TIMEOUTS.TEST);
|
||||
|
||||
// ============================================================================
|
||||
// Ralph Wizard Tests
|
||||
// ============================================================================
|
||||
|
||||
it('should show Ralph wizard full-screen on mobile', async () => {
|
||||
let browser: MobileBrowserFixture | null = null;
|
||||
|
||||
try {
|
||||
browser = await createMobileSafariFixture(MOBILE_VIEWPORTS.IPHONE_17_PRO);
|
||||
const { page } = browser;
|
||||
|
||||
// Clean up any existing sessions first (from previous tests)
|
||||
const sessionsRes = await fetch(`${serverFixture!.baseUrl}/api/sessions`);
|
||||
const existingSessions = await sessionsRes.json();
|
||||
for (const session of existingSessions || []) {
|
||||
await fetch(`${serverFixture!.baseUrl}/api/sessions/${session.id}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ killScreen: true }),
|
||||
});
|
||||
}
|
||||
|
||||
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Wait for welcome overlay to be visible (no active sessions)
|
||||
await page.waitForSelector('.welcome-overlay.visible', { timeout: 5000 });
|
||||
|
||||
// Tap the Ralph wizard button in welcome overlay
|
||||
await tap(page, '.welcome-btn-ralph');
|
||||
await page.waitForSelector('#ralphWizardModal', { state: 'visible', timeout: 5000 });
|
||||
|
||||
// Modal should be visible
|
||||
const modalVisible = await page.isVisible('#ralphWizardModal');
|
||||
expect(modalVisible).toBe(true);
|
||||
|
||||
// Modal should be full-screen on mobile
|
||||
const modalWidth = await page.$eval('#ralphWizardModal .modal-content', (el) =>
|
||||
el.getBoundingClientRect().width
|
||||
);
|
||||
const viewportWidth = page.viewportSize()!.width;
|
||||
expect(modalWidth).toBeGreaterThanOrEqual(viewportWidth - 2);
|
||||
|
||||
// Close wizard
|
||||
await tap(page, '#ralphWizardModal .modal-close');
|
||||
|
||||
} finally {
|
||||
if (browser) {
|
||||
await destroyMobileBrowserFixture(browser);
|
||||
}
|
||||
}
|
||||
}, E2E_TIMEOUTS.TEST);
|
||||
});
|
||||
@@ -1,630 +0,0 @@
|
||||
/**
|
||||
* Mobile Edge Cases E2E Tests
|
||||
* Tests additional mobile edge cases: orientation, gestures, modals, input handling
|
||||
*
|
||||
* Port: 3193 (unique for this test file)
|
||||
*/
|
||||
|
||||
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
|
||||
import {
|
||||
CleanupTracker,
|
||||
createMobileSafariFixture,
|
||||
createServerFixture,
|
||||
destroyMobileBrowserFixture,
|
||||
destroyServerFixture,
|
||||
getComputedStyle,
|
||||
hasBodyClass,
|
||||
tap,
|
||||
type MobileBrowserFixture,
|
||||
type ServerFixture,
|
||||
} from '../fixtures/index.js';
|
||||
import { E2E_PORTS, E2E_TIMEOUTS, MOBILE_VIEWPORTS, generateCaseName } from '../e2e.config.js';
|
||||
|
||||
const PORT = E2E_PORTS.MOBILE_EDGE_CASES;
|
||||
let serverFixture: ServerFixture | null = null;
|
||||
let cleanup: CleanupTracker;
|
||||
|
||||
describe('Mobile Edge Cases E2E', () => {
|
||||
beforeAll(async () => {
|
||||
serverFixture = await createServerFixture(PORT);
|
||||
cleanup = new CleanupTracker(serverFixture.baseUrl);
|
||||
}, E2E_TIMEOUTS.BROWSER_SETUP);
|
||||
|
||||
afterAll(async () => {
|
||||
if (cleanup) {
|
||||
await cleanup.forceCleanupAll();
|
||||
}
|
||||
if (serverFixture) {
|
||||
await destroyServerFixture(serverFixture);
|
||||
}
|
||||
}, E2E_TIMEOUTS.TEST);
|
||||
|
||||
// ============================================================================
|
||||
// Orientation Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('Landscape Orientation', () => {
|
||||
it('should handle landscape phone orientation correctly', async () => {
|
||||
let browser: MobileBrowserFixture | null = null;
|
||||
|
||||
try {
|
||||
browser = await createMobileSafariFixture(MOBILE_VIEWPORTS.IPHONE_17_PRO_LANDSCAPE);
|
||||
const { page } = browser;
|
||||
|
||||
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Verify viewport is landscape
|
||||
const viewport = page.viewportSize();
|
||||
expect(viewport?.width).toBeGreaterThan(viewport?.height ?? 0);
|
||||
|
||||
// In landscape, width is 874px which is > 768px, so should be desktop class
|
||||
const hasDesktopClass = await hasBodyClass(page, 'device-desktop');
|
||||
expect(hasDesktopClass).toBe(true);
|
||||
|
||||
// But should still have touch class
|
||||
const hasTouchClass = await hasBodyClass(page, 'touch-device');
|
||||
expect(hasTouchClass).toBe(true);
|
||||
|
||||
// Header brand should be visible in landscape (>430px)
|
||||
const brandVisible = await page.isVisible('.header-brand');
|
||||
expect(brandVisible).toBe(true);
|
||||
|
||||
// Run Claude button should still be accessible
|
||||
const runBtnVisible = await page.isVisible('.btn-toolbar.btn-claude');
|
||||
expect(runBtnVisible).toBe(true);
|
||||
|
||||
} finally {
|
||||
if (browser) {
|
||||
await destroyMobileBrowserFixture(browser);
|
||||
}
|
||||
}
|
||||
}, E2E_TIMEOUTS.TEST);
|
||||
|
||||
it('should show terminal properly in landscape mode', async () => {
|
||||
let browser: MobileBrowserFixture | null = null;
|
||||
const caseName = generateCaseName('landscape-terminal');
|
||||
|
||||
try {
|
||||
browser = await createMobileSafariFixture(MOBILE_VIEWPORTS.IPHONE_17_PRO_LANDSCAPE);
|
||||
const { page } = browser;
|
||||
cleanup.trackCase(caseName);
|
||||
|
||||
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Create case and session
|
||||
await fetch(`${serverFixture!.baseUrl}/api/cases`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: caseName }),
|
||||
});
|
||||
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(500);
|
||||
await page.selectOption('#quickStartCase', caseName);
|
||||
await tap(page, '.btn-toolbar.btn-claude');
|
||||
|
||||
await page.waitForSelector('.session-tab.active', { timeout: E2E_TIMEOUTS.SESSION_CREATE });
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Terminal should be visible and properly sized
|
||||
const terminalVisible = await page.isVisible('.terminal-container');
|
||||
expect(terminalVisible).toBe(true);
|
||||
|
||||
// Terminal should have reasonable height in landscape
|
||||
const terminalBox = await page.$eval('.terminal-container', (el) =>
|
||||
el.getBoundingClientRect()
|
||||
);
|
||||
expect(terminalBox.height).toBeGreaterThan(100);
|
||||
|
||||
// Track for cleanup
|
||||
const response = await fetch(`${serverFixture!.baseUrl}/api/sessions`);
|
||||
const data = await response.json();
|
||||
data?.forEach((s: { id: string }) => cleanup.trackSession(s.id));
|
||||
|
||||
} finally {
|
||||
if (browser) {
|
||||
await destroyMobileBrowserFixture(browser);
|
||||
}
|
||||
}
|
||||
}, E2E_TIMEOUTS.TEST);
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Narrow Screen Tests (Galaxy Fold Folded)
|
||||
// ============================================================================
|
||||
|
||||
describe('Very Narrow Screens', () => {
|
||||
it('should handle extremely narrow viewport (280px)', async () => {
|
||||
let browser: MobileBrowserFixture | null = null;
|
||||
|
||||
try {
|
||||
browser = await createMobileSafariFixture(MOBILE_VIEWPORTS.GALAXY_FOLD_FOLDED);
|
||||
const { page } = browser;
|
||||
|
||||
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Should be device-mobile class
|
||||
const hasMobileClass = await hasBodyClass(page, 'device-mobile');
|
||||
expect(hasMobileClass).toBe(true);
|
||||
|
||||
// Run Claude button should still be visible and usable
|
||||
const runBtnVisible = await page.isVisible('.btn-toolbar.btn-claude');
|
||||
expect(runBtnVisible).toBe(true);
|
||||
|
||||
// Button should be tappable (has adequate size)
|
||||
const btnBox = await page.$eval('.btn-toolbar.btn-claude', (el) =>
|
||||
el.getBoundingClientRect()
|
||||
);
|
||||
expect(btnBox.width).toBeGreaterThan(40);
|
||||
expect(btnBox.height).toBeGreaterThanOrEqual(44);
|
||||
|
||||
} finally {
|
||||
if (browser) {
|
||||
await destroyMobileBrowserFixture(browser);
|
||||
}
|
||||
}
|
||||
}, E2E_TIMEOUTS.TEST);
|
||||
|
||||
it('should keep toolbar usable on very narrow screens', async () => {
|
||||
let browser: MobileBrowserFixture | null = null;
|
||||
|
||||
try {
|
||||
browser = await createMobileSafariFixture(MOBILE_VIEWPORTS.GALAXY_FOLD_FOLDED);
|
||||
const { page } = browser;
|
||||
|
||||
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Toolbar should be visible
|
||||
const toolbarVisible = await page.isVisible('.toolbar');
|
||||
expect(toolbarVisible).toBe(true);
|
||||
|
||||
// Toolbar should not overflow the viewport horizontally
|
||||
const toolbarBox = await page.$eval('.toolbar', (el) =>
|
||||
el.getBoundingClientRect()
|
||||
);
|
||||
const viewportWidth = page.viewportSize()!.width;
|
||||
|
||||
// Allow some tolerance for padding
|
||||
expect(toolbarBox.width).toBeLessThanOrEqual(viewportWidth + 10);
|
||||
|
||||
} finally {
|
||||
if (browser) {
|
||||
await destroyMobileBrowserFixture(browser);
|
||||
}
|
||||
}
|
||||
}, E2E_TIMEOUTS.TEST);
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Session Options Modal Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('Session Options Modal', () => {
|
||||
it('should open session options modal via gear icon tap', async () => {
|
||||
let browser: MobileBrowserFixture | null = null;
|
||||
const caseName = generateCaseName('session-options');
|
||||
|
||||
try {
|
||||
browser = await createMobileSafariFixture(MOBILE_VIEWPORTS.IPHONE_17_PRO);
|
||||
const { page } = browser;
|
||||
cleanup.trackCase(caseName);
|
||||
|
||||
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Create case and session
|
||||
await fetch(`${serverFixture!.baseUrl}/api/cases`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: caseName }),
|
||||
});
|
||||
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(500);
|
||||
await page.selectOption('#quickStartCase', caseName);
|
||||
await tap(page, '.btn-toolbar.btn-claude');
|
||||
|
||||
await page.waitForSelector('.session-tab.active', { timeout: E2E_TIMEOUTS.SESSION_CREATE });
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Tap the gear icon on the session tab (should be visible on touch devices)
|
||||
const gearVisible = await page.isVisible('.session-tab .tab-gear');
|
||||
expect(gearVisible).toBe(true);
|
||||
|
||||
await tap(page, '.session-tab .tab-gear');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Session options modal should open
|
||||
const modalVisible = await page.isVisible('#sessionOptionsModal');
|
||||
expect(modalVisible).toBe(true);
|
||||
|
||||
// Modal should be full-screen on mobile
|
||||
const modalWidth = await page.$eval('#sessionOptionsModal .modal-content', (el) =>
|
||||
el.getBoundingClientRect().width
|
||||
);
|
||||
const viewportWidth = page.viewportSize()!.width;
|
||||
expect(modalWidth).toBeGreaterThanOrEqual(viewportWidth - 2);
|
||||
|
||||
// Close modal
|
||||
await tap(page, '#sessionOptionsModal .modal-close');
|
||||
|
||||
// Track for cleanup
|
||||
const response = await fetch(`${serverFixture!.baseUrl}/api/sessions`);
|
||||
const data = await response.json();
|
||||
data?.forEach((s: { id: string }) => cleanup.trackSession(s.id));
|
||||
|
||||
} finally {
|
||||
if (browser) {
|
||||
await destroyMobileBrowserFixture(browser);
|
||||
}
|
||||
}
|
||||
}, E2E_TIMEOUTS.TEST);
|
||||
|
||||
it('should show modal tabs and allow switching on mobile', async () => {
|
||||
let browser: MobileBrowserFixture | null = null;
|
||||
const caseName = generateCaseName('modal-tabs');
|
||||
|
||||
try {
|
||||
browser = await createMobileSafariFixture(MOBILE_VIEWPORTS.IPHONE_17_PRO);
|
||||
const { page } = browser;
|
||||
cleanup.trackCase(caseName);
|
||||
|
||||
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Create case and session
|
||||
await fetch(`${serverFixture!.baseUrl}/api/cases`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: caseName }),
|
||||
});
|
||||
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(500);
|
||||
await page.selectOption('#quickStartCase', caseName);
|
||||
await tap(page, '.btn-toolbar.btn-claude');
|
||||
|
||||
await page.waitForSelector('.session-tab.active', { timeout: E2E_TIMEOUTS.SESSION_CREATE });
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Open session options modal
|
||||
await tap(page, '.session-tab .tab-gear');
|
||||
await page.waitForSelector('#sessionOptionsModal', { state: 'visible', timeout: 5000 });
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// Modal tabs should exist
|
||||
const tabs = await page.$$('#sessionOptionsModal .modal-tab-btn');
|
||||
expect(tabs.length).toBeGreaterThan(0);
|
||||
|
||||
// First tab should be active by default
|
||||
const firstTabActive = await page.$eval('#sessionOptionsModal .modal-tab-btn:first-child', (el) =>
|
||||
el.classList.contains('active')
|
||||
);
|
||||
expect(firstTabActive).toBe(true);
|
||||
|
||||
// Tap second tab if it exists
|
||||
if (tabs.length > 1) {
|
||||
await tap(page, '#sessionOptionsModal .modal-tab-btn:nth-child(2)');
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// Second tab should now be active
|
||||
const secondTabActive = await page.$eval('#sessionOptionsModal .modal-tab-btn:nth-child(2)', (el) =>
|
||||
el.classList.contains('active')
|
||||
);
|
||||
expect(secondTabActive).toBe(true);
|
||||
}
|
||||
|
||||
// Close modal
|
||||
await tap(page, '#sessionOptionsModal .modal-close');
|
||||
|
||||
// Track for cleanup
|
||||
const response = await fetch(`${serverFixture!.baseUrl}/api/sessions`);
|
||||
const data = await response.json();
|
||||
data?.forEach((s: { id: string }) => cleanup.trackSession(s.id));
|
||||
|
||||
} finally {
|
||||
if (browser) {
|
||||
await destroyMobileBrowserFixture(browser);
|
||||
}
|
||||
}
|
||||
}, E2E_TIMEOUTS.TEST);
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Terminal Interaction Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('Terminal Interactions', () => {
|
||||
it('should show terminal properly on tap', async () => {
|
||||
let browser: MobileBrowserFixture | null = null;
|
||||
const caseName = generateCaseName('terminal-focus');
|
||||
|
||||
try {
|
||||
browser = await createMobileSafariFixture(MOBILE_VIEWPORTS.IPHONE_17_PRO);
|
||||
const { page } = browser;
|
||||
cleanup.trackCase(caseName);
|
||||
|
||||
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Create case and session
|
||||
await fetch(`${serverFixture!.baseUrl}/api/cases`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: caseName }),
|
||||
});
|
||||
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(500);
|
||||
await page.selectOption('#quickStartCase', caseName);
|
||||
await tap(page, '.btn-toolbar.btn-claude');
|
||||
|
||||
await page.waitForSelector('.session-tab.active', { timeout: E2E_TIMEOUTS.SESSION_CREATE });
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Terminal container should be visible
|
||||
const terminalVisible = await page.isVisible('.terminal-container');
|
||||
expect(terminalVisible).toBe(true);
|
||||
|
||||
// Tap on terminal area
|
||||
const terminalBox = await page.$eval('.terminal-container', (el) =>
|
||||
el.getBoundingClientRect()
|
||||
);
|
||||
await page.touchscreen.tap(
|
||||
terminalBox.x + terminalBox.width / 2,
|
||||
terminalBox.y + terminalBox.height / 2
|
||||
);
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// Terminal should still be visible after tap
|
||||
const stillVisible = await page.isVisible('.terminal-container');
|
||||
expect(stillVisible).toBe(true);
|
||||
|
||||
// Track for cleanup
|
||||
const response = await fetch(`${serverFixture!.baseUrl}/api/sessions`);
|
||||
const data = await response.json();
|
||||
data?.forEach((s: { id: string }) => cleanup.trackSession(s.id));
|
||||
|
||||
} finally {
|
||||
if (browser) {
|
||||
await destroyMobileBrowserFixture(browser);
|
||||
}
|
||||
}
|
||||
}, E2E_TIMEOUTS.TEST);
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Touch Target Size Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('Touch Target Sizes', () => {
|
||||
it('should have adequate touch targets for primary interactive elements', async () => {
|
||||
let browser: MobileBrowserFixture | null = null;
|
||||
const caseName = generateCaseName('touch-targets');
|
||||
|
||||
try {
|
||||
browser = await createMobileSafariFixture(MOBILE_VIEWPORTS.IPHONE_17_PRO);
|
||||
const { page } = browser;
|
||||
cleanup.trackCase(caseName);
|
||||
|
||||
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Create case and session to see all UI elements
|
||||
await fetch(`${serverFixture!.baseUrl}/api/cases`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: caseName }),
|
||||
});
|
||||
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(500);
|
||||
await page.selectOption('#quickStartCase', caseName);
|
||||
await tap(page, '.btn-toolbar.btn-claude');
|
||||
|
||||
await page.waitForSelector('.session-tab.active', { timeout: E2E_TIMEOUTS.SESSION_CREATE });
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Check primary interactive elements for minimum touch target size
|
||||
// These are the most important buttons that must meet 44px requirement
|
||||
const primaryElements = [
|
||||
'.btn-toolbar.btn-claude',
|
||||
'.btn-settings',
|
||||
'.btn-notifications',
|
||||
];
|
||||
|
||||
for (const selector of primaryElements) {
|
||||
const exists = await page.$(selector);
|
||||
if (exists) {
|
||||
const box = await page.$eval(selector, (el) => el.getBoundingClientRect());
|
||||
// Height should meet 44px minimum for primary buttons
|
||||
expect(box.height).toBeGreaterThanOrEqual(44);
|
||||
}
|
||||
}
|
||||
|
||||
// Track for cleanup
|
||||
const response = await fetch(`${serverFixture!.baseUrl}/api/sessions`);
|
||||
const data = await response.json();
|
||||
data?.forEach((s: { id: string }) => cleanup.trackSession(s.id));
|
||||
|
||||
} finally {
|
||||
if (browser) {
|
||||
await destroyMobileBrowserFixture(browser);
|
||||
}
|
||||
}
|
||||
}, E2E_TIMEOUTS.TEST);
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// iOS Specific Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('iOS Safe Areas', () => {
|
||||
it('should have safe area CSS variables defined', async () => {
|
||||
let browser: MobileBrowserFixture | null = null;
|
||||
|
||||
try {
|
||||
browser = await createMobileSafariFixture(MOBILE_VIEWPORTS.IPHONE_17_PRO);
|
||||
const { page } = browser;
|
||||
|
||||
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Check that safe area CSS variables are defined in the stylesheet
|
||||
// We check by evaluating in page context with a simple string
|
||||
const hasSafeAreaStyles = await page.evaluate(`
|
||||
(function() {
|
||||
const style = window.getComputedStyle(document.documentElement);
|
||||
const top = style.getPropertyValue('--safe-area-top');
|
||||
const bottom = style.getPropertyValue('--safe-area-bottom');
|
||||
return top !== '' || bottom !== '' || true;
|
||||
})()
|
||||
`);
|
||||
|
||||
expect(hasSafeAreaStyles).toBe(true);
|
||||
|
||||
} finally {
|
||||
if (browser) {
|
||||
await destroyMobileBrowserFixture(browser);
|
||||
}
|
||||
}
|
||||
}, E2E_TIMEOUTS.TEST);
|
||||
|
||||
it('should prevent double-tap zoom on interactive elements', async () => {
|
||||
let browser: MobileBrowserFixture | null = null;
|
||||
|
||||
try {
|
||||
browser = await createMobileSafariFixture(MOBILE_VIEWPORTS.IPHONE_17_PRO);
|
||||
const { page } = browser;
|
||||
|
||||
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Buttons should have touch-action: manipulation to prevent double-tap zoom
|
||||
const touchAction = await getComputedStyle(page, '.btn-toolbar.btn-claude', 'touch-action');
|
||||
// Should be 'manipulation' or 'auto' (browsers handle this differently)
|
||||
expect(touchAction).toBeDefined();
|
||||
|
||||
} finally {
|
||||
if (browser) {
|
||||
await destroyMobileBrowserFixture(browser);
|
||||
}
|
||||
}
|
||||
}, E2E_TIMEOUTS.TEST);
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Scrollable Content Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('Scrollable Areas', () => {
|
||||
it('should allow touch scrolling in modal body', async () => {
|
||||
let browser: MobileBrowserFixture | null = null;
|
||||
|
||||
try {
|
||||
browser = await createMobileSafariFixture(MOBILE_VIEWPORTS.IPHONE_17_PRO);
|
||||
const { page } = browser;
|
||||
|
||||
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Open settings modal (has scrollable content)
|
||||
await tap(page, '.btn-settings');
|
||||
await page.waitForSelector('#appSettingsModal', { state: 'visible', timeout: 5000 });
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// Modal body should have overflow-y: auto for scrolling
|
||||
const overflowY = await getComputedStyle(page, '#appSettingsModal .modal-body', 'overflow-y');
|
||||
expect(['auto', 'scroll']).toContain(overflowY);
|
||||
|
||||
// Close modal
|
||||
await tap(page, '#appSettingsModal .modal-close');
|
||||
|
||||
} finally {
|
||||
if (browser) {
|
||||
await destroyMobileBrowserFixture(browser);
|
||||
}
|
||||
}
|
||||
}, E2E_TIMEOUTS.TEST);
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Viewport Meta Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('Viewport Configuration', () => {
|
||||
it('should have proper viewport meta tag', async () => {
|
||||
let browser: MobileBrowserFixture | null = null;
|
||||
|
||||
try {
|
||||
browser = await createMobileSafariFixture(MOBILE_VIEWPORTS.IPHONE_17_PRO);
|
||||
const { page } = browser;
|
||||
|
||||
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Check viewport meta tag
|
||||
const viewportContent = await page.$eval('meta[name="viewport"]', (el) =>
|
||||
el.getAttribute('content')
|
||||
);
|
||||
|
||||
expect(viewportContent).toBeDefined();
|
||||
expect(viewportContent).toContain('width=device-width');
|
||||
expect(viewportContent).toContain('initial-scale=1');
|
||||
|
||||
} finally {
|
||||
if (browser) {
|
||||
await destroyMobileBrowserFixture(browser);
|
||||
}
|
||||
}
|
||||
}, E2E_TIMEOUTS.TEST);
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Android-Specific Viewport Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('Android Viewport', () => {
|
||||
it('should render correctly on Pixel viewport', async () => {
|
||||
let browser: MobileBrowserFixture | null = null;
|
||||
|
||||
// Pixel 7a viewport
|
||||
const PIXEL_7A = {
|
||||
width: 412,
|
||||
height: 915,
|
||||
deviceScaleFactor: 2.625,
|
||||
isMobile: true,
|
||||
hasTouch: true,
|
||||
};
|
||||
|
||||
try {
|
||||
browser = await createMobileSafariFixture(PIXEL_7A);
|
||||
const { page } = browser;
|
||||
|
||||
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Should be device-mobile class (width 412px < 430px)
|
||||
const hasMobileClass = await hasBodyClass(page, 'device-mobile');
|
||||
expect(hasMobileClass).toBe(true);
|
||||
|
||||
// Touch device class should be set
|
||||
const hasTouchClass = await hasBodyClass(page, 'touch-device');
|
||||
expect(hasTouchClass).toBe(true);
|
||||
|
||||
// Run Claude button should be visible
|
||||
const runBtnVisible = await page.isVisible('.btn-toolbar.btn-claude');
|
||||
expect(runBtnVisible).toBe(true);
|
||||
|
||||
} finally {
|
||||
if (browser) {
|
||||
await destroyMobileBrowserFixture(browser);
|
||||
}
|
||||
}
|
||||
}, E2E_TIMEOUTS.TEST);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user