Danh sách bài viết

Bài 10: Option Fixture geolocation, permissions

Bài 10 nhóm Fixtures - Options. geolocation và permissions là 2 option fixture cho phép emulate vị trí GPS và tự động grant browser permission mà không cần user click "Allow". Hai option này thường phải dùng song song: set geolocation chỉ đặt tọa độ mock — nếu thiếu permissions: ['geolocation'], navigator.geolocation.getCurrentPosition() vẫn nhận callback PERMISSION_DENIED. Object geolocation gồm latitude (-90..90), longitude (-180..180), accuracy optional (mặc định 0 — perfect fix). permissions nhận array string: 'geolocation', 'camera', 'microphone', 'notifications', 'clipboard-read', 'clipboard-write', 'background-sync', 'midi', 'midi-sysex', 'accelerometer', 'gyroscope', 'magnetometer', 'storage-access', 'persistent-storage', 'ambient-light-sensor', 'payment-handler'. Use case: location-based feature (store locator, delivery zone, weather), camera/microphone call app, notification UI, clipboard copy-paste. Pattern per-describe: mỗi describe block dùng test.use riêng để simulate nhiều user region. Runtime override: context.setGeolocation() thay tọa độ giữa test mà không cần tạo context mới. Grant per origin: context.grantPermissions(['geolocation'], { origin: 'https://app.com' }). Camera/microphone cần thêm launchOptions.args fake media stream flags. Limitations: watchPosition không tự trigger khi toạ độ đổi — phải gọi setGeolocation explicit. Bài này tập trung vào góc độ fixture composition và pattern nâng cao; các bài cơ sở trong Series 1 (bài 325, 326) đã cover syntax và runtime API riêng lẻ.

27/05/2026
14 phút đọc
0 lượt xem
1

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

  • Hiểu tại sao geolocationpermissions phải đi cùng nhau và cách Playwright xử lý hai option này ở tầng BrowserContext.
  • Nắm cấu trúc đầy đủ của geolocation object: latitude, longitude, accuracy.
  • Liệt kê được các giá trị hợp lệ trong permissions array.
  • Áp dụng pattern per-describe để simulate nhiều user region trong cùng file test.
  • Dùng context.setGeolocation() override tọa độ runtime để simulate user movement.
  • Grant permission per origin với context.grantPermissions(..., { origin }).
  • Cấu hình fake media stream cho test camera/microphone.
  • Nhận biết 4 pitfall phổ biến và giới hạn kỹ thuật của geolocation + permissions.
2

Tại Sao Phải Dùng Cả Hai Cùng Lúc

Browser tách biệt hai khái niệm:

  • Mock data: tọa độ GPS nào sẽ trả về — do geolocation option kiểm soát.
  • Permission: trang web có được phép gọi Geolocation API không — do permission model của browser kiểm soát.

Khi user thật truy cập web, browser hiện popup "Allow this site to know your location?". Nếu user từ chối, API nhận POSITION_UNAVAILABLE hoặc PERMISSION_DENIED — bất kể GPS device có tọa độ hay không.

Trong test, hai cơ chế đó vẫn hoạt động độc lập. Set geolocation chỉ cung cấp dữ liệu tọa độ — nó không tự grant permission. Nếu bỏ qua permissions: ['geolocation']:

// Sai — chỉ set toạ độ, chưa grant permission
use: {
  geolocation: { latitude: 21.0285, longitude: 105.8542 },
  // permissions bị thiếu
}

App vẫn nhận error callback vì browser chưa grant. Đúng phải là:

use: {
  geolocation: { latitude: 21.0285, longitude: 105.8542 },
  permissions: ['geolocation'],
}

Playwright implement cả hai ở tầng BrowserContext. Khi dùng qua use trong config hoặc test.use, context được khởi tạo với tọa độ sẵn có và permission đã granted — app gọi navigator.geolocation.getCurrentPosition() ngay lập tức nhận tọa độ mock, không có popup nào xuất hiện.

3

Cấu Trúc Object geolocation

interface Geolocation {
  latitude: number;   // -90 đến 90 (bắt buộc)
  longitude: number;  // -180 đến 180 (bắt buộc)
  accuracy?: number;  // độ chính xác tính theo mét (tuỳ chọn, default: 0)
}

latitude và longitude

Hai field bắt buộc, kiểu number. Playwright sẽ throw nếu truyền string:

// Đúng
geolocation: { latitude: 21.0285, longitude: 105.8542 }

// Sai — TypeScript error, Playwright reject ở runtime
geolocation: { latitude: '21.0285', longitude: '105.8542' }

Phạm vi hợp lệ tuân theo Geolocation API spec. Latitude vượt ngoài [-90, 90] hoặc longitude vượt ngoài [-180, 180] sẽ bị Playwright từ chối ngay khi khởi tạo context.

accuracy

Giá trị tính bằng mét, không âm. Mặc định 0 — tương đương "perfect fix". Thực tế GPS device thường có accuracy 3–15 m trong điều kiện tốt. Nếu app filter kết quả theo accuracy (chỉ chấp nhận fix nhỏ hơn 20 m), test cần set explicit:

geolocation: {
  latitude: 21.0285,
  longitude: 105.8542,
  accuracy: 10,   // 10 mét
}

Một số app nghi ngờ accuracy = 0 vì GPS thật không bao giờ perfect — nên set accuracy hợp lý để tránh app bỏ qua fix.

4

Danh Sách permissions Hỗ Trợ

permissions nhận array string. Giá trị phải đúng chính xác (lowercase, có dấu gạch ngang):

type PermissionName =
  | 'geolocation'
  | 'camera'
  | 'microphone'
  | 'notifications'
  | 'background-sync'
  | 'midi'
  | 'midi-sysex'
  | 'accelerometer'
  | 'gyroscope'
  | 'magnetometer'
  | 'storage-access'
  | 'persistent-storage'
  | 'ambient-light-sensor'
  | 'payment-handler'
  | 'clipboard-read'
  | 'clipboard-write';

Lưu ý về browser support:

  • Chromium hỗ trợ toàn bộ danh sách trên.
  • Firefox và WebKit hỗ trợ một phần — geolocation, notifications, camera, microphone thường có; sensor API (accelerometer, gyroscope…) phụ thuộc engine.
  • Nếu pass permission name không hợp lệ, Playwright throw error ngay khi setup.

Grant nhiều permission cùng lúc:

permissions: ['geolocation', 'notifications', 'clipboard-read', 'clipboard-write']

Không cần liệt kê permission không dùng — chỉ grant những gì test cần.

5

Config Mức Global

Đặt trong playwright.config.ts khi toàn bộ project dùng một location mặc định:

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  use: {
    geolocation: { latitude: 21.0285, longitude: 105.8542 },
    permissions: ['geolocation'],
  },
});

Mọi test sẽ chạy như user đứng tại Hà Nội (lat 21.0285, lng 105.8542). Project có nhiều region có thể split thành nhiều project config:

export default defineConfig({
  projects: [
    {
      name: 'hanoi',
      use: {
        geolocation: { latitude: 21.0285, longitude: 105.8542 },
        permissions: ['geolocation'],
      },
      testMatch: '**/location/**/*.spec.ts',
    },
    {
      name: 'hcm',
      use: {
        geolocation: { latitude: 10.7769, longitude: 106.7009 },
        permissions: ['geolocation'],
      },
      testMatch: '**/location/**/*.spec.ts',
    },
  ],
});

Pattern này chạy cùng một bộ test với hai tọa độ khác nhau, giúp phát hiện bug logic "vùng miền" mà không cần duplicate test code.

6

Pattern Per-Describe — Multi-Region

Khi test nhiều region trong cùng file, dùng test.use trong từng describe block. Mỗi describe tạo một context riêng với fixture override tương ứng:

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

test.describe('Hanoi user', () => {
  test.use({
    geolocation: { latitude: 21.0285, longitude: 105.8542 },
    permissions: ['geolocation'],
    locale: 'vi-VN',
    timezoneId: 'Asia/Ho_Chi_Minh',
  });

  test('shows Hanoi stores', async ({ page }) => {
    await page.goto('/stores');
    await page.getByRole('button', { name: 'Find near me' }).click();
    await expect(page.getByText(/Hanoi|Hà Nội/)).toBeVisible();
  });

  test('shows VND currency', async ({ page }) => {
    await page.goto('/checkout');
    await expect(page.getByText('VND')).toBeVisible();
  });
});

test.describe('Tokyo user', () => {
  test.use({
    geolocation: { latitude: 35.6762, longitude: 139.6503 },
    permissions: ['geolocation'],
    locale: 'ja-JP',
    timezoneId: 'Asia/Tokyo',
  });

  test('shows Tokyo stores', async ({ page }) => {
    await page.goto('/stores');
    await page.getByRole('button', { name: 'Find near me' }).click();
    await expect(page.getByText(/Tokyo|東京/)).toBeVisible();
  });

  test('shows JPY currency', async ({ page }) => {
    await page.goto('/checkout');
    await expect(page.getByText('JPY')).toBeVisible();
  });
});

test.use trong describe chỉ apply cho test nằm trong block đó. Các describe khác dùng giá trị mặc định hoặc override riêng — không bị ảnh hưởng lẫn nhau.

Pattern này kết hợp tốt với localetimezoneId (bài 9) để simulate người dùng tại vùng cụ thể một cách đầy đủ.

7

Verify Location-Based Feature

Khi test store locator hoặc delivery zone, không nên assert tọa độ thô — nên assert kết quả nghiệp vụ mà app hiển thị:

test('show stores near location', async ({ page }) => {
  await page.goto('/stores');
  await page.getByRole('button', { name: 'Find near me' }).click();

  // navigator.geolocation tự trả tọa độ đã set
  await expect(page.getByText(/Hanoi|Hà Nội/)).toBeVisible();
});

Khi nút "Find near me" được click, app gọi navigator.geolocation.getCurrentPosition(). Playwright intercept call này và trả tọa độ mock (21.0285, 105.8542). App backend nhận tọa độ, tìm store gần nhất, trả về danh sách — test verify danh sách chứa "Hà Nội".

Với weather widget hoặc delivery zone, pattern tương tự — click "Use my location" → app lấy tọa độ → hiển thị content phù hợp:

test('weather widget shows Hanoi', async ({ page }) => {
  await page.goto('/');
  // App auto-request geolocation khi load
  await expect(page.getByTestId('weather-city')).toHaveText('Hà Nội');
  await expect(page.getByTestId('weather-temp')).toBeVisible();
});

Một số app request geolocation ngay khi load — không cần thao tác click. Cần đảm bảo permission đã grant trước khi page.goto() được gọi — khi dùng option fixture, context đã có permission sẵn nên không cần thêm bước.

8

Runtime Override: context.setGeolocation()

context.setGeolocation() thay tọa độ giữa test mà không cần tạo context mới. Hữu ích khi test feature tracking hoặc delivery simulation cần user "di chuyển":

test('move user simulation', async ({ context, page }) => {
  // Vị trí ban đầu — Trung tâm Hà Nội
  await context.setGeolocation({ latitude: 21.0285, longitude: 105.8542 });
  await page.goto('/track');

  await expect(page.getByTestId('current-zone')).toHaveText('Hoàn Kiếm');

  // Di chuyển lên phía Bắc
  await context.setGeolocation({ latitude: 21.0500, longitude: 105.9000 });
  await page.reload();

  await expect(page.getByTestId('current-zone')).toHaveText('Long Biên');
});

Một số điểm cần lưu ý:

  • setGeolocation() chỉ thay dữ liệu mock — không tự trigger lại API call. App cần reload hoặc gọi lại navigator.geolocation.getCurrentPosition() để nhận tọa độ mới.
  • Nếu app dùng watchPosition(), tọa độ mới không tự push tới callback sau khi setGeolocation() — đây là limitation (xem bài 12).
  • Permission không cần grant lại — một lần grant ở context level là đủ cho toàn bộ vòng đời context.
9

Grant Per Origin

Mặc định, permissions option grant cho tất cả origin trong context. Để giới hạn một domain cụ thể, dùng context.grantPermissions() với origin:

test('grant only for main app', async ({ context, page }) => {
  // Grant geolocation chỉ cho app.com, không cho cdn hay iframe từ domain khác
  await context.grantPermissions(['geolocation'], { origin: 'https://app.com' });

  await page.goto('https://app.com/map');
  // navigator.geolocation hoạt động ở đây

  // Nếu page load iframe từ map.third-party.com
  // Iframe đó KHÔNG có permission geolocation vì chưa grant riêng
});

Clear toàn bộ permission để reset về trạng thái mặc định:

await context.clearPermissions();

clearPermissions() reset tất cả permission trong context, không filter được từng permission riêng. Nếu cần revoke một permission cụ thể, phải clear hết rồi grant lại những cái cần giữ.

Trường hợp dùng origin-specific grant trong fixture option không khả dụng — permissions array trong use không nhận origin. Khi cần per-origin, phải dùng context.grantPermissions() runtime trong test body.

10

Camera & Microphone — Fake Media Stream

Grant cameramicrophone permission chỉ bỏ popup "Allow" — app vẫn cần media device thật để stream. Trong môi trường CI không có camera, cần bật fake device:

// playwright.config.ts
use: {
  permissions: ['camera', 'microphone'],
  launchOptions: {
    args: [
      '--use-fake-ui-for-media-stream',    // Tự accept media stream request (không có popup UI)
      '--use-fake-device-for-media-stream', // Dùng fake device thay camera/mic thật
    ],
  },
}

Hai flag này là Chromium-specific:

  • --use-fake-ui-for-media-stream: bỏ qua dialog "which camera/mic to use".
  • --use-fake-device-for-media-stream: inject một fake video/audio track — app nhận MediaStream hợp lệ nhưng là nội dung giả (thường là màn hình xanh có timestamp).

Pattern test video call feature:

test('user can join video call', async ({ page }) => {
  await page.goto('/meeting/room-123');
  await page.getByRole('button', { name: 'Join' }).click();

  // Không có popup permission, không cần camera thật
  await expect(page.getByTestId('video-track')).toBeVisible();
  await expect(page.getByTestId('mic-indicator')).toHaveAttribute('data-active', 'true');
});

Lưu ý: hai flag trên không có trên Firefox/WebKit. Test camera/microphone trên Firefox/WebKit cần cơ chế khác (mock MediaDevices API qua page.addInitScript).

11

Notification & Clipboard

Notification

Grant notifications để test trang yêu cầu notification permission và hiển thị UI tương ứng:

test.use({ permissions: ['notifications'] });

test('notification permission banner không hiện', async ({ page }) => {
  await page.goto('/dashboard');
  // App check Notification.permission === 'granted'
  // Không hiện banner "Enable notifications?"
  await expect(page.getByText('Enable notifications?')).not.toBeVisible();
});

test('notification permission granted từ đầu', async ({ page }) => {
  await page.goto('/settings');
  const permission = await page.evaluate(() => Notification.permission);
  expect(permission).toBe('granted');
});

Clipboard

Grant cả clipboard-readclipboard-write khi test copy-paste feature:

test.use({ permissions: ['clipboard-read', 'clipboard-write'] });

test('copy button writes to clipboard', async ({ page }) => {
  await page.goto('/invoice/123');
  await page.getByRole('button', { name: 'Copy link' }).click();

  const clipboardText = await page.evaluate(() => navigator.clipboard.readText());
  expect(clipboardText).toContain('https://app.com/invoice/123');
});

Không grant clipboard-read, gọi navigator.clipboard.readText() trong page.evaluate sẽ throw DOMException NotAllowedError.

12

Limitations

watchPosition không tự trigger

navigator.geolocation.watchPosition() đăng ký callback nhận cập nhật vị trí liên tục. Trên device thật, GPS liên tục push vị trí mới vào callback. Với Playwright, gọi context.setGeolocation() thay dữ liệu mock nhưng không trigger callback của watchPosition. App không nhận vị trí mới cho đến khi app tự gọi getCurrentPosition() lại, hoặc dev mock watchPosition thủ công qua page.addInitScript.

// Vấn đề: watchPosition không nhận update sau setGeolocation
test('simulate real-time tracking — cần workaround', async ({ context, page }) => {
  await context.setGeolocation({ latitude: 21.0285, longitude: 105.8542 });
  await page.goto('/live-track');

  // App dùng watchPosition — callback chạy khi load
  await expect(page.getByTestId('coords')).toContainText('21.0285');

  // Đổi toạ độ
  await context.setGeolocation({ latitude: 21.0500, longitude: 105.9000 });

  // watchPosition callback KHÔNG tự fire — coords vẫn hiện 21.0285
  // Cần page.reload() hoặc trigger lại getCurrentPosition từ app
  await page.reload();
  await expect(page.getByTestId('coords')).toContainText('21.0500');
});

permissions phải liệt kê từng permission

Không có shorthand grant "all" — phải liệt kê cụ thể từng permission cần grant.

Một số API chưa được support

Bluetooth, USB, NFC, wake-lock chưa nằm trong danh sách permission Playwright hỗ trợ. Test các feature này cần mock thủ công ở tầng API.

camera/microphone fake stream không apply trên Firefox/WebKit

--use-fake-device-for-media-stream là Chromium flag — Firefox và WebKit không nhận. Test media call cross-browser cần approach khác.

13

4 Pitfalls

Pitfall 1 — Set geolocation nhưng quên permissions

// Lỗi thường gặp nhất
use: {
  geolocation: { latitude: 21.0285, longitude: 105.8542 },
  // Thiếu: permissions: ['geolocation']
}
// Kết quả: navigator.geolocation.getCurrentPosition() → error callback PERMISSION_DENIED

Fix: luôn đặt permissions: ['geolocation'] khi dùng geolocation.

Pitfall 2 — Grant camera nhưng quên launchOptions fake stream

// Grant permission nhưng không có fake device
use: {
  permissions: ['camera', 'microphone'],
  // Thiếu launchOptions.args fake stream
}
// Kết quả: getUserMedia() throw NotFoundError hoặc app hiện UI lỗi thiếu camera

Fix: thêm launchOptions.args với hai fake stream flags khi test camera/mic trên Chromium.

Pitfall 3 — Truyền permissions là string thay vì array

// TypeScript error
use: {
  permissions: 'geolocation',  // Sai kiểu — phải là string[]
}

// Đúng
use: {
  permissions: ['geolocation'],
}

TypeScript compiler sẽ báo lỗi, nhưng nếu bỏ qua type check hoặc dùng JavaScript, Playwright sẽ throw runtime error.

Pitfall 4 — Simulate movement nhưng quên trigger lại location read

test('track movement', async ({ context, page }) => {
  await context.setGeolocation({ latitude: 21.0285, longitude: 105.8542 });
  await page.goto('/track');

  await context.setGeolocation({ latitude: 21.0500, longitude: 105.9000 });
  // THIẾU: page.reload() hoặc trigger lại getCurrentPosition từ app
  // Test expect tọa độ mới nhưng app vẫn hiện tọa độ cũ

  await expect(page.getByTestId('lat')).toContainText('21.0500'); // FAIL
});

Fix: sau setGeolocation(), cần trigger app đọc lại vị trí — thường là page.reload() hoặc click button "Refresh location" nếu app có.

14

Quiz + Bài Tiếp

Quiz

  1. Config sau có vấn đề gì không?

    use: {
      geolocation: { latitude: 21.0285, longitude: 105.8542 },
      permissions: ['geolocation'],
    }
    Đáp án

    Không có vấn đề — đây là cấu hình tối thiểu đúng. Có thể thêm accuracy nếu app filter theo độ chính xác, nhưng không bắt buộc.

  2. Gọi context.setGeolocation({ latitude: 21.05, longitude: 105.9 }) sau khi test đã bắt đầu. App đang dùng watchPosition(). Callback có nhận tọa độ mới không?

    Đáp án

    Không. setGeolocation() thay dữ liệu mock nhưng không trigger callback của watchPosition(). Cần reload page hoặc app gọi lại getCurrentPosition() thì mới nhận tọa độ mới.

  3. Để test camera trên Chromium CI, config tối thiểu cần gì ngoài permissions: ['camera']?

    Đáp án

    Thêm launchOptions.args: ['--use-fake-ui-for-media-stream', '--use-fake-device-for-media-stream']. Hai flag này inject fake media device để getUserMedia() không throw NotFoundError.

  4. context.clearPermissions() có thể revoke chỉ geolocation mà giữ lại notifications không?

    Đáp án

    Không. clearPermissions() reset toàn bộ permission trong context. Để giữ lại một phần, phải clear hết rồi grant lại những permission muốn giữ.

  5. Khi dùng test.use({ permissions: ['geolocation'] }) trong một describe block, các test ngoài block đó có bị ảnh hưởng không?

    Đáp án

    Không. test.use() trong describe chỉ apply cho test nằm trong block đó. Test ngoài block dùng giá trị mặc định của project config.

Bài Tiếp

Bài 11: Option Fixture colorScheme — emulate giao diện dark mode / light mode / no-preference để test CSS media query prefers-color-scheme.