Danh sách bài viết

Bài 55: Mobile Project Với devices

Bài này tập trung vào cách cấu hình project mobile trong playwright.config.ts dùng devices presets — mỗi preset đóng gói sẵn viewport, userAgent, deviceScaleFactor, isMobile, hasTouch và defaultBrowserType. Trọng tâm nằm ở phần áp dụng vào projects[]: desktop + mobile matrix, mobile-only suite với testMatch, override engine cho từng device, và cách viết test cross-device dùng fixture isMobile. Phần cuối là 4 pitfall điển hình và quiz 4 câu.

28/05/2026
0 lượt xem
1

Mục Tiêu Bài Học

Sau khi hoàn thành bài này, bạn sẽ:

  • Cấu hình project mobile trong projects[] bằng cách spread devices preset vào use.
  • Biết các preset phổ biến: iPhone 15 Pro, iPhone SE, Pixel 7, iPad Pro 11, Galaxy S9+.
  • Xây dựng desktop + mobile matrix với 4 project trong cùng một config.
  • Hiểu quy tắc browser engine mặc định: iPhone/iPad → WebKit, Pixel/Galaxy → Chromium.
  • Override engine per device khi cần test trên engine không mặc định.
  • Cấu hình mobile-only suite dùng testMatch để isolate file test mobile.
  • Viết test cross-device bằng fixture isMobile để branch logic.
  • Hiểu behavior của isMobilehasTouch ảnh hưởng đến CSS và event.
  • Tránh 4 pitfall: quên spread, test logic sai trên mobile layout, engine không tương thích, conflict snapshot name.
2

Tại Sao Cần Project Mobile Riêng

Bài 54 đã xây dựng multi-browser matrix với Desktop Chrome, Desktop Firefox, Desktop Safari. Ba project đó khác nhau về engine nhưng cùng viewport desktop và đều không có touch. Mobile project khác hẳn ở chỗ preset đóng gói nhiều thứ cùng lúc:

  • Viewport nhỏ — kích hoạt breakpoint CSS mobile (thường < 768px).
  • userAgent mobile — server-side code detect đúng mobile, không bị fallback về desktop layout.
  • deviceScaleFactor (DPR) — ảnh hưởng đến lựa chọn ảnh srcset, tính toán canvas, visual regression screenshot pixel.
  • isMobile: true — browser xử lý <meta name="viewport"> đúng như mobile browser, kích hoạt media query (hover: none).
  • hasTouch: true — phát sinh touch event, navigator.maxTouchPoints > 0.

Nếu chỉ set viewport: { width: 390, height: 844 } trong project mà không dùng preset, bạn chỉ thu hẹp viewport — UA vẫn là desktop, isMobilehasTouch vẫn false. Server sẽ không nhận diện là mobile, media query touch-based sẽ không fire, touch event không có. Dùng devices[name] là cách duy nhất để kích hoạt đầy đủ emulation mobile.

Về tổ chức config: thêm project mobile vào projects[] là ít xâm lấn nhất — không cần tạo file config riêng, không break project desktop hiện có. Mỗi test trong testDir tự động chạy trên cả project desktop lẫn mobile trừ khi bạn giới hạn bằng testMatch.

3

Cú Pháp Cơ Bản — Spread Preset Vào Project

Import devices cùng với defineConfig từ @playwright/test:

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  projects: [
    // Desktop baseline
    { name: 'desktop-chrome', use: { ...devices['Desktop Chrome'] } },

    // Mobile devices
    { name: 'iphone-15', use: { ...devices['iPhone 15 Pro'] } },
    { name: 'pixel-7', use: { ...devices['Pixel 7'] } },
    { name: 'ipad-pro', use: { ...devices['iPad Pro 11'] } },
  ],
});

Spread operator ...devices['iPhone 15 Pro'] rải toàn bộ 6 field của preset vào object use của project: viewport, userAgent, deviceScaleFactor, isMobile, hasTouch, defaultBrowserType. Khi test runner khởi động project, nó đọc defaultBrowserType để launch đúng engine, rồi truyền các field còn lại vào browser.newContext().

Có thể thêm option sau spread để override hoặc bổ sung:

{
  name: 'iphone-15-vi',
  use: {
    ...devices['iPhone 15 Pro'],
    locale: 'vi-VN',
    timezoneId: 'Asia/Ho_Chi_Minh',
  },
}

Thứ tự quan trọng: field đặt sau spread sẽ override field cùng tên trong preset. Đặt trước spread thì sẽ bị preset ghi đè.

4

Popular Presets — iPhone, Pixel, iPad, Galaxy

Dưới đây là các preset mobile dùng phổ biến nhất trong project thực tế, cùng với giá trị chính:

iPhone series

Preset Viewport (CSS px) DPR Engine
iPhone 15 Pro 393 × 852 3 webkit
iPhone 14 390 × 844 3 webkit
iPhone SE 375 × 667 2 webkit

Android / Google

Preset Viewport (CSS px) DPR Engine
Pixel 7 412 × 915 2.625 chromium
Pixel 5 393 × 851 2.75 chromium

iPad

Preset Viewport (CSS px) DPR Engine
iPad Pro 11 834 × 1194 2 webkit
iPad Mini 768 × 1024 2 webkit

Samsung

Preset Viewport (CSS px) DPR Engine
Galaxy S9+ 320 × 658 4.5 chromium
Galaxy Note 3 360 × 640 3 chromium

Một điểm lưu ý: iPad có isMobile: truehasTouch: true nhưng viewport rộng hơn nhiều so với phone — breakpoint desktop (768px+) thường active trên iPad. App responsive cần kiểm tra riêng breakpoint tablet vì layout iPad thường khác cả mobile lẫn desktop.

Galaxy S9+ có DPR 4.5 — một trong những DPR cao nhất trong danh sách preset. Visual regression screenshot trên preset này sẽ lớn hơn đáng kể so với iPhone SE (DPR 2). Cần lưu ý khi quản lý file snapshot.

5

Desktop + Mobile Matrix

Pattern phổ biến nhất khi bắt đầu thêm coverage mobile: giữ nguyên project desktop, thêm mobile theo cặp iOS + Android:

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  use: {
    baseURL: 'https://example.com',
  },
  projects: [
    // Desktop
    { name: 'desktop-chrome', use: { ...devices['Desktop Chrome'] } },
    { name: 'desktop-firefox', use: { ...devices['Desktop Firefox'] } },

    // Mobile — iOS (WebKit) + Android (Chromium)
    { name: 'mobile-safari', use: { ...devices['iPhone 15 Pro'] } },
    { name: 'mobile-chrome', use: { ...devices['Pixel 7'] } },
  ],
});

Cặp iPhone 15 Pro (WebKit) + Pixel 7 (Chromium) bao phủ hai engine chính trên mobile. Một test suite có 50 file → 50 × 4 project = 200 test runs, chạy song song tùy số worker.

Chạy chỉ project mobile:

# Một project cụ thể
npx playwright test --project='mobile-safari'

# Tất cả project mobile theo wildcard
npx playwright test --project='mobile-*'

# Chỉ desktop
npx playwright test --project='desktop-*'

Thêm iPad vào matrix để cover breakpoint tablet:

projects: [
  { name: 'desktop-chrome', use: { ...devices['Desktop Chrome'] } },
  { name: 'mobile-safari', use: { ...devices['iPhone 15 Pro'] } },
  { name: 'mobile-chrome', use: { ...devices['Pixel 7'] } },
  { name: 'tablet-safari', use: { ...devices['iPad Pro 11'] } },
],

CI thực tế thường không chạy tất cả matrix mỗi PR. Pattern phổ biến: PR chạy desktop-chrome + mobile-safari, nightly chạy toàn matrix. Kết hợp với tag bài 44–45 và project filter để tối ưu thời gian CI.

6

Browser Engine Mặc Định Của Mỗi Preset

Field defaultBrowserType trong preset quyết định engine nào được launch khi test runner khởi động project đó. Quy tắc:

  • iPhone, iPad (Apple) → webkit — mô phỏng Safari iOS.
  • Pixel, Galaxy, Nexus, Moto (Android) → chromium — mô phỏng Chrome Android.
  • Desktop Chrome, Edgechromium.
  • Desktop Firefoxfirefox.
  • Desktop Safariwebkit.

Điều này quan trọng vì hai lý do:

Thứ nhất, CI phải install đúng binary. Nếu thêm iPhone 15 Pro vào project mà CI chỉ install Chromium, test sẽ fail với lỗi "WebKit executable not found":

# Install tất cả engine (phù hợp khi chạy full matrix)
npx playwright install

# Install chỉ webkit (cho pipeline chuyên mobile iOS)
npx playwright install webkit

# Install webkit kèm system dependencies
npx playwright install --with-deps webkit

Thứ hai, render khác biệt thực chất giữa WebKit và Chromium — không chỉ là cosmetic. Safari iOS có các quirks về CSS sticky, scroll behavior, auto-play policy, font rendering, và một số API web không có hoặc khác spec. Test pass trên Chromium không đảm bảo pass trên WebKit.

7

Override Engine Per Device

Có thể override defaultBrowserType bằng cách đặt browserName sau spread:

// Chạy iPhone preset nhưng dùng Chromium (không phải WebKit)
{
  name: 'iphone-on-chromium',
  use: {
    ...devices['iPhone 15 Pro'],
    browserName: 'chromium',
  },
}

Khi nào dùng override:

  • Debug nhanh trên CI thiếu WebKit — tạm thời switch sang Chromium để kiểm tra logic, không phải render Safari-specific.
  • Test viewport mobile + engine desktop — verify app xử lý đúng UA mobile mà không phụ thuộc quirk WebKit. Ví dụ test redirect logic server-side chỉ cần UA mobile, không cần chính xác engine.
  • So sánh behavior giữa engine — tạo hai project cùng preset nhưng khác engine để isolate bug.

Lưu ý quan trọng: browserName phải là 'chromium', 'firefox', hoặc 'webkit' — không phải tên brand như 'chrome' hay 'safari'. Sai tên → lỗi launch ngay lúc khởi động test runner.

// Sai — 'chrome' không phải tên engine hợp lệ
use: { ...devices['iPhone 15 Pro'], browserName: 'chrome' }

// Đúng
use: { ...devices['iPhone 15 Pro'], browserName: 'chromium' }
8

Mobile-Only Suite Với testMatch

Khi test chỉ có ý nghĩa trên mobile (gesture, hamburger menu, bottom navigation), chạy chúng trên desktop project lãng phí thời gian và sinh kết quả sai. Dùng testMatch để giới hạn project chỉ chạy file test nhất định:

projects: [
  // Project mobile chỉ chạy file có "mobile" trong tên
  {
    name: 'mobile-suite',
    testMatch: /.*mobile.*\.spec\.ts/,
    use: { ...devices['iPhone 15 Pro'] },
  },

  // Project desktop bỏ qua file mobile
  {
    name: 'desktop-chrome',
    testIgnore: /.*mobile.*\.spec\.ts/,
    use: { ...devices['Desktop Chrome'] },
  },

  // Project chạy tất cả test (không phân biệt)
  {
    name: 'desktop-firefox',
    use: { ...devices['Desktop Firefox'] },
  },
],

Convention đặt tên file phổ biến:

tests/
  auth.spec.ts          # Chạy trên tất cả project
  checkout.spec.ts      # Chạy trên tất cả project
  mobile/
    hamburger.mobile.spec.ts   # Chỉ mobile project
    bottom-nav.mobile.spec.ts  # Chỉ mobile project
  desktop/
    sidebar.desktop.spec.ts    # Chỉ desktop project

testMatch nhận regex hoặc glob string hoặc array. Một số pattern hay dùng:

// Tất cả file .mobile.spec.ts
testMatch: /.*\.mobile\.spec\.ts/

// Tất cả file trong thư mục tests/mobile/
testMatch: '**/mobile/**/*.spec.ts'

// Array — nhiều pattern
testMatch: ['**/*.mobile.spec.ts', '**/mobile/*.spec.ts']
9

Test Cross-Device Với Fixture isMobile

Fixture isMobile là boolean built-in của Playwright Test, phản ánh giá trị isMobile của context hiện tại. Dùng để viết test một lần nhưng branch logic theo device:

import { test, expect } from '@playwright/test';

test('responsive nav', async ({ page, isMobile }) => {
  await page.goto('/');

  if (isMobile) {
    // Mobile: desktop nav ẩn, hamburger button hiển thị
    await expect(page.getByRole('navigation')).not.toBeVisible();
    await page.getByRole('button', { name: 'Menu' }).click();
    await expect(page.getByRole('navigation')).toBeVisible();
  } else {
    // Desktop: navigation luôn visible
    await expect(page.getByRole('navigation')).toBeVisible();
  }
});

Test này chạy trên cả 4 project trong matrix — mỗi lần isMobile mang giá trị tương ứng với preset của project đó. Project desktop-chromeisMobile = false, project mobile-safariisMobile = true.

Pattern skip test nếu không phải mobile:

test('mobile bottom nav', async ({ page, isMobile }) => {
  test.skip(!isMobile, 'Chỉ test trên mobile');

  await page.goto('/');
  await expect(page.getByTestId('bottom-nav')).toBeVisible();
});

Pattern ngược lại — skip nếu là mobile:

test('desktop sidebar', async ({ page, isMobile }) => {
  test.skip(isMobile, 'Chỉ test trên desktop');

  await page.goto('/');
  await expect(page.getByTestId('sidebar')).toBeVisible();
});

So với testMatch ở trên: testMatch isolate hoàn toàn file, trong khi isMobile trong test body linh hoạt hơn — một file test có thể chứa cả test shared lẫn test branch theo device. Chọn cách nào tùy vào mức độ tách biệt bạn muốn.

10

Behavior Của isMobile Và hasTouch

Hai flag này ảnh hưởng đến nhiều thứ trong browser context, quan trọng khi debug test fail chỉ trên mobile project:

isMobile — ảnh hưởng CSS và viewport

  • Media query (hover: hover)false khi isMobile: true. CSS rule dùng @media (hover: hover) sẽ không apply.
  • CSS pseudo-class :hover không áp dụng trên mobile emulation — phần tử không nhận hover state khi di chuột qua (vì không có mouse, chỉ có touch).
  • <meta name="viewport" content="width=device-width"> được xử lý đúng như mobile browser thật.
  • Chỉ Chromium hỗ trợ đầy đủ isMobile. WebKit và Firefox có thể xử lý một số aspect khác.

hasTouch — ảnh hưởng event

  • Touch event (touchstart, touchmove, touchend) được phát sinh khi interact.
  • navigator.maxTouchPoints trả về giá trị > 0. JavaScript trên trang có thể dùng điều này để detect touch capability và bật/tắt feature.
  • Click event vẫn hoạt động (Playwright tổng hợp click từ touch sequence), nhưng code frontend check ontouchstart in window hoặc navigator.maxTouchPoints sẽ thấy touch.

Ví dụ thực tế: button tooltip chỉ hiển thị khi hover trên desktop, không hiển thị trên mobile:

test('tooltip behavior', async ({ page, isMobile }) => {
  await page.goto('/');
  const helpIcon = page.getByTestId('help-icon');

  if (!isMobile) {
    // Desktop: hover trigger tooltip
    await helpIcon.hover();
    await expect(page.getByRole('tooltip')).toBeVisible();
  } else {
    // Mobile: hover không hoạt động, tooltip không trigger
    await helpIcon.hover();
    await expect(page.getByRole('tooltip')).not.toBeVisible();
  }
});
11

Use Cases Khi Dùng Mobile Project

Responsive layout test

Verify layout không vỡ khi viewport thu nhỏ — elements không bị cắt, text không overlap, button vẫn tappable (44px × 44px tối thiểu theo guideline HIG). Desktop test hoàn toàn pass vẫn có thể fail trên mobile nếu có element bị hidden bởi display: none ở breakpoint mobile.

Mobile-specific UI components

Hamburger menu, bottom navigation bar, mobile drawer, swipe-to-delete row, pull-to-refresh — các component này chỉ render hoặc chỉ hoạt động đúng khi isMobile: true hoặc viewport đủ nhỏ để trigger breakpoint.

Touch gesture test

Carousel swipe, pinch-to-zoom, long-press context menu cần hasTouch: true. Chi tiết API gesture (touchscreen.tap(), dispatchEvent touch sequence) thuộc Series 1 bài 202–207 — bài này chỉ lưu ý rằng không dùng preset có touch thì gesture test sẽ silent fail vì touch event không phát sinh.

PWA install flow test

Browser chỉ hiển thị PWA install prompt (Add to Home Screen) khi UA là mobile. Desktop Chrome cũng có install nhưng flow UX khác. Preset iPhone/Pixel trigger đúng UA mobile → test được install prompt đúng flow mobile.

Visual regression cross-device

Screenshot trên iPhone 15 Pro (393px, DPR 3) và Pixel 7 (412px, DPR 2.625) khác nhau cả về kích thước và pixel density. Playwright lưu snapshot theo project name — file hero.spec.ts-snapshots/hero-mobile-safari-linux.png riêng biệt với hero-desktop-chrome-linux.png. Cần commit cả hai bộ snapshot vào repo.

12

Limitation — Emulation Không Thay Thế Được Device Thật

  • Emulation ≠ hardware thật — CPU throttle, GPU rendering, battery API, accelerometer, NFC, bluetooth, biometric không emulate được. Bug liên quan đến hiệu năng thật hoặc sensor phần cứng sẽ không catch được.
  • WebKit trên Linux kém ổn định hơn macOS cho iPhone preset — Playwright CI chạy Linux. WebKit binary trên Linux là port riêng, không phải Safari iOS thật. Một số rendering và API behavior sẽ khác với Safari trên iPhone thật hoặc Safari macOS. Test fail trên iPhone preset Linux CI nhưng pass trên device thật là scenario có thể xảy ra.
  • Touch event không isTrusted — Playwright tổng hợp touch event có Event.isTrusted = false. Một số payment gateway hoặc captcha sử dụng isTrusted để phát hiện automation. Test sẽ không bắt được loại block này (Series 1 bài 207).
  • App native wrapper (React Native, Flutter Web) không emulate được — mobile emulation chỉ áp dụng cho web browser. App đóng gói trong WebView native container hoặc dùng canvas rendering không bị ảnh hưởng bởi preset.

Emulation phù hợp nhất để test responsive layout, UA-based logic, touch event của web app tiêu chuẩn. Với bug hardware-specific hoặc platform-specific Safari iOS, cần test trên BrowserStack / Sauce Labs với device thật hoặc Xcode Simulator.

13

4 Pitfall

Pitfall 1 — Quên spread, chỉ set viewport

// Sai — chỉ viewport, không có UA/touch/isMobile
{
  name: 'mobile',
  use: {
    viewport: { width: 393, height: 852 },
  },
}

// Đúng — spread toàn bộ preset
{
  name: 'mobile',
  use: { ...devices['iPhone 15 Pro'] },
}

Khi chỉ set viewport, userAgent vẫn là desktop Chrome, isMobilehasTouch đều false. Server-side UA detection sẽ không nhận diện mobile. Touch event không hoạt động. Test "responsive" sẽ chạy được nhưng không phản ánh đúng behavior mobile.

Pitfall 2 — Test desktop logic chạy trên mobile project gây fail không mong đợi

Test click vào element ở vị trí desktop layout nhưng trên mobile layout element đó bị hidden hoặc di chuyển sang vị trí khác:

// Test này pass trên desktop nhưng fail trên mobile
// vì trên mobile, sidebar không render
test('sidebar link', async ({ page }) => {
  await page.goto('/');
  await page.getByTestId('sidebar-link').click(); // Not found trên mobile
  await expect(page).toHaveURL('/products');
});

Giải pháp: dùng isMobile để branch, hoặc dùng testMatch / testIgnore để giới hạn project, hoặc thêm test.skip(isMobile).

Pitfall 3 — Force browserName không tương thích với binary chưa install

// Override engine sang firefox
{
  name: 'iphone-firefox',
  use: { ...devices['iPhone 15 Pro'], browserName: 'firefox' },
}
// → Fail nếu firefox binary chưa install

Firefox không hỗ trợ đầy đủ mobile emulation (isMobile bị bỏ qua một phần). Kết hợp iPhone preset với Firefox thường không có ý nghĩa thực tế về emulation, và cần đảm bảo binary đã install. Chỉ override engine sang chromium hoặc webkit cho mobile preset.

Pitfall 4 — Visual snapshot conflict khi thiếu project name trong snapshot path

Playwright tự động include project name trong tên file snapshot. Nếu đặt tên project giống nhau (ví dụ 'chromium') cho cả desktop và mobile project, snapshot có thể overwrite nhau:

// Nguy hiểm — cùng tên project
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'chromium', use: { ...devices['Pixel 7'] } },  // Trùng tên!

// Tốt hơn — tên rõ ràng
{ name: 'desktop-chrome', use: { ...devices['Desktop Chrome'] } },
{ name: 'mobile-chrome', use: { ...devices['Pixel 7'] } },

Playwright thực ra không cho phép hai project cùng tên — sẽ báo lỗi config. Nhưng nếu dùng tên gần giống như 'chrome''Chrome' (chỉ khác case), trên filesystem case-insensitive (macOS default) snapshot file có thể overwrite nhau im lặng.

14

Quiz

Câu 1. Config sau có vấn đề gì?

projects: [
  {
    name: 'mobile',
    use: {
      viewport: { width: 390, height: 844 },
      isMobile: true,
    },
  },
],
Đáp án

Thiếu userAgent mobile, hasTouch, deviceScaleFactor, và defaultBrowserType. Server-side UA detection sẽ không nhận diện mobile. Touch event không hoạt động vì thiếu hasTouch: true. Đúng ra phải dùng { ...devices['iPhone 14'] } (hoặc preset phù hợp) để có đầy đủ các field.

Câu 2. Project { name: 'tablet', use: { ...devices['iPad Pro 11'] } } sẽ launch engine nào? CI cần install gì?

Đáp án

Launch webkit vì iPad Pro 11 là preset Apple → defaultBrowserType: 'webkit'. CI cần chạy npx playwright install webkit (hoặc npx playwright install --with-deps webkit trên Ubuntu để có đủ system dependency).

Câu 3. Bạn có test chạy tốt trên desktop-chrome project nhưng fail trên mobile-safari project với lỗi "Element not found: [data-testid=sidebar]". Nguyên nhân khả năng cao nhất là gì, và giải pháp?

Đáp án

Sidebar ẩn trên mobile layout (CSS display: none ở breakpoint nhỏ). Giải pháp: thêm test.skip(isMobile, 'Sidebar không render trên mobile') trong test, hoặc dùng testIgnore trên mobile project để không chạy file test desktop-specific, hoặc viết lại test branch theo isMobile fixture.

Câu 4. Có project config như sau:

{
  name: 'iphone-chromium-test',
  use: { ...devices['iPhone 15 Pro'], browserName: 'chromium' },
}

Project này launch Chromium hay WebKit? UA string của context sẽ là UA của iPhone hay desktop Chrome?

Đáp án

Launch ChromiumbrowserName: 'chromium' đặt sau spread override defaultBrowserType: 'webkit' của preset. Nhưng UA string vẫn là UA của iPhone vì field userAgent trong preset được giữ lại (không bị override). Context sẽ gửi iPhone UA đến server nhưng dùng Chromium engine để render.

15

Bài Tiếp Theo