.claude/skills/mobile-emulation/SKILL.md
Mobile device emulation and responsive testing with Playwright. Use when testing mobile layouts, touch interactions, device-specific features, or responsive breakpoints.
npx skillsauth add adaptationio/skrillz mobile-emulationInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Test responsive designs and mobile-specific features using Playwright's device emulation capabilities.
import { test, expect, devices } from '@playwright/test';
test.use(devices['iPhone 14']);
test('mobile navigation works', async ({ page }) => {
await page.goto('/');
// Mobile menu should be visible
await page.getByRole('button', { name: 'Menu' }).click();
await expect(page.getByRole('navigation')).toBeVisible();
});
playwright.config.ts:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
projects: [
// Desktop browsers
{
name: 'Desktop Chrome',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'Desktop Firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'Desktop Safari',
use: { ...devices['Desktop Safari'] },
},
// Mobile devices
{
name: 'iPhone 14',
use: { ...devices['iPhone 14'] },
},
{
name: 'iPhone 14 Pro Max',
use: { ...devices['iPhone 14 Pro Max'] },
},
{
name: 'Pixel 7',
use: { ...devices['Pixel 7'] },
},
{
name: 'Galaxy S23',
use: { ...devices['Galaxy S III'] }, // Closest available
},
// Tablets
{
name: 'iPad Pro',
use: { ...devices['iPad Pro 11'] },
},
{
name: 'iPad Mini',
use: { ...devices['iPad Mini'] },
},
],
});
{
name: 'Custom Mobile',
use: {
viewport: { width: 390, height: 844 },
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)...',
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
defaultBrowserType: 'webkit',
},
},
import { devices } from '@playwright/test';
// iPhones
devices['iPhone 14']
devices['iPhone 14 Plus']
devices['iPhone 14 Pro']
devices['iPhone 14 Pro Max']
devices['iPhone 13']
devices['iPhone 12']
devices['iPhone SE']
// Android Phones
devices['Pixel 7']
devices['Pixel 5']
devices['Galaxy S III']
devices['Galaxy S5']
devices['Galaxy Note 3']
devices['Nexus 5']
// Tablets
devices['iPad Pro 11']
devices['iPad Pro 11 landscape']
devices['iPad Mini']
devices['iPad (gen 7)']
devices['Galaxy Tab S4']
// Desktop
devices['Desktop Chrome']
devices['Desktop Firefox']
devices['Desktop Safari']
devices['Desktop Edge']
import { devices } from '@playwright/test';
console.log(Object.keys(devices));
// Outputs all available device names
const breakpoints = [
{ name: 'mobile', width: 375, height: 667 },
{ name: 'tablet', width: 768, height: 1024 },
{ name: 'desktop', width: 1280, height: 720 },
{ name: 'wide', width: 1920, height: 1080 },
];
for (const bp of breakpoints) {
test(`layout at ${bp.name}`, async ({ page }) => {
await page.setViewportSize({ width: bp.width, height: bp.height });
await page.goto('/');
await expect(page).toHaveScreenshot(`layout-${bp.name}.png`);
});
}
test('responsive navigation', async ({ page }) => {
await page.goto('/');
// Desktop - horizontal nav
await page.setViewportSize({ width: 1280, height: 720 });
await expect(page.locator('.desktop-nav')).toBeVisible();
await expect(page.locator('.mobile-menu-button')).not.toBeVisible();
// Tablet - may show hamburger
await page.setViewportSize({ width: 768, height: 1024 });
// Mobile - hamburger menu
await page.setViewportSize({ width: 375, height: 667 });
await expect(page.locator('.desktop-nav')).not.toBeVisible();
await expect(page.locator('.mobile-menu-button')).toBeVisible();
});
test('tap interaction', async ({ page }) => {
await page.goto('/');
// Tap is equivalent to click on touch devices
await page.getByRole('button').tap();
});
test('swipe carousel', async ({ page }) => {
await page.goto('/gallery');
const carousel = page.locator('.carousel');
const box = await carousel.boundingBox();
if (box) {
// Swipe left
await page.mouse.move(box.x + box.width - 50, box.y + box.height / 2);
await page.mouse.down();
await page.mouse.move(box.x + 50, box.y + box.height / 2, { steps: 10 });
await page.mouse.up();
}
await expect(page.locator('.slide-2')).toBeVisible();
});
test('pinch to zoom map', async ({ page }) => {
await page.goto('/map');
const map = page.locator('#map');
const box = await map.boundingBox();
if (box) {
const centerX = box.x + box.width / 2;
const centerY = box.y + box.height / 2;
// Simulate pinch out (zoom in)
await page.touchscreen.tap(centerX, centerY);
// Note: Multi-touch pinch requires custom implementation
}
});
test('long press context menu', async ({ page }) => {
await page.goto('/');
const element = page.locator('.long-press-target');
const box = await element.boundingBox();
if (box) {
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
await page.mouse.down();
await page.waitForTimeout(1000); // Hold for 1 second
await page.mouse.up();
}
await expect(page.locator('.context-menu')).toBeVisible();
});
test('orientation change', async ({ page }) => {
// Portrait
await page.setViewportSize({ width: 390, height: 844 });
await page.goto('/video');
await expect(page.locator('.video-container')).toHaveCSS('width', '390px');
// Landscape
await page.setViewportSize({ width: 844, height: 390 });
await expect(page.locator('.video-container')).toHaveCSS('width', '844px');
});
test.use(devices['iPad Pro 11 landscape']);
test('tablet landscape layout', async ({ page }) => {
await page.goto('/dashboard');
// Sidebar should be visible in landscape
await expect(page.locator('.sidebar')).toBeVisible();
});
test.use({
geolocation: { latitude: 40.7128, longitude: -74.0060 }, // NYC
permissions: ['geolocation'],
});
test('shows nearby locations', async ({ page }) => {
await page.goto('/locations');
await page.getByRole('button', { name: 'Find Nearby' }).click();
await expect(page.getByText('New York')).toBeVisible();
});
test('location change', async ({ page, context }) => {
await context.setGeolocation({ latitude: 51.5074, longitude: -0.1278 }); // London
await page.goto('/weather');
await expect(page.getByText('London')).toBeVisible();
await context.setGeolocation({ latitude: 35.6762, longitude: 139.6503 }); // Tokyo
await page.reload();
await expect(page.getByText('Tokyo')).toBeVisible();
});
test('works on slow network', async ({ page, context }) => {
// Emulate slow 3G
const client = await context.newCDPSession(page);
await client.send('Network.emulateNetworkConditions', {
offline: false,
downloadThroughput: (500 * 1024) / 8, // 500kb/s
uploadThroughput: (500 * 1024) / 8,
latency: 400, // 400ms
});
await page.goto('/');
// Should show skeleton loaders
await expect(page.locator('.skeleton')).toBeVisible();
// Eventually loads
await expect(page.locator('.content')).toBeVisible({ timeout: 30000 });
});
test('offline functionality', async ({ page, context }) => {
await page.goto('/');
// Cache page, then go offline
await context.setOffline(true);
await page.reload();
// Should show offline message or cached content
await expect(page.getByText(/offline/i)).toBeVisible();
});
test('respects safe areas', async ({ page }) => {
// iPhone with notch
test.use(devices['iPhone 14 Pro']);
await page.goto('/');
// Header should account for notch
const header = page.locator('header');
const paddingTop = await header.evaluate(el =>
window.getComputedStyle(el).paddingTop
);
// Should have safe area inset
expect(parseInt(paddingTop)).toBeGreaterThan(20);
});
test.use({
colorScheme: 'dark',
});
test('dark mode styling', async ({ page }) => {
await page.goto('/');
const body = page.locator('body');
const bgColor = await body.evaluate(el =>
window.getComputedStyle(el).backgroundColor
);
// Should have dark background
expect(bgColor).toBe('rgb(0, 0, 0)'); // or dark color
});
test.use({
reducedMotion: 'reduce',
});
test('respects reduced motion', async ({ page }) => {
await page.goto('/');
const animated = page.locator('.animated-element');
const animationDuration = await animated.evaluate(el =>
window.getComputedStyle(el).animationDuration
);
// Should have no animation
expect(animationDuration).toBe('0s');
});
const testDevices = [
'iPhone 14',
'Pixel 7',
'iPad Pro 11',
'Desktop Chrome',
];
for (const deviceName of testDevices) {
test.describe(`Visual: ${deviceName}`, () => {
test.use(devices[deviceName]);
test('homepage', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveScreenshot(`homepage-${deviceName}.png`);
});
test('product page', async ({ page }) => {
await page.goto('/products/1');
await expect(page).toHaveScreenshot(`product-${deviceName}.png`);
});
});
}
references/device-list.md - Complete device list with specsreferences/touch-patterns.md - Touch gesture implementationsdevelopment
Setup secure web-based terminal access to WSL2 from mobile/tablet via ttyd + ngrok/Cloudflare/Tailscale. One-command install, start, stop, status. Use when you need remote terminal access, web terminal, browser-based shell, or mobile access to WSL2 environment.
development
Complete development workflows where Claude writes the code while Gemini and Codex provide research, planning, reviews, and different perspectives. Claude remains the main developer. Use for complex projects requiring expert planning and multi-perspective reviews.
development
Systematic progress tracking for skill development. Manages task states (pending/in_progress/completed), updates in real-time, reports progress, identifies blockers, and maintains momentum. Use when tracking skill development, coordinating work, or reporting progress.
testing
Comprehensive testing workflow orchestrating functional testing, example validation, integration testing, and usability assessment. Sequential workflow for complete skill testing from examples through scenarios to integration validation. Use when conducting thorough testing, pre-deployment validation, ensuring skill functionality, or comprehensive quality checks.