NỘI DUNG BÀI HỌC
Hiểu rõ 'Actionability' ⏳✅
Từ click() đến dragTo() 🖱️🖐️
Gửi phím tắt hiệu quả ⌨️⚡
Phần 1: Làm chủ các Hành động với Chuột (Mouse Actions) trong Playwright
Playwright mô phỏng các hành động của chuột (như click, di chuột, kéo thả) giống hệt như một người dùng thật. Để làm được điều này, nó sử dụng một cơ chế tự động chờ mạnh mẽ gọi là "Actionability".
Khái niệm cốt lõi: "Actionability" (Khả năng tương tác)
Đây là điều quan trọng nhất bạn cần hiểu. Khi bạn gọi một hành động liên quan đến con trỏ (như click()), Playwright sẽ không hành động ngay lập tức. Thay vào đó, "dưới mui xe" (under the hood), nó tự động thực hiện một loạt các bước kiểm tra:
- Chờ element có trong DOM: Đảm bảo element tồn tại.
- Chờ element được hiển thị: Đợi cho đến khi nó không bị ẩn (display: none, visibility: hidden) và có kích thước (không phải 0x0).
- Chờ element ngừng di chuyển: Đợi cho đến khi các hiệu ứng CSS (transition) hoặc animation kết thúc.
- Cuộn element vào tầm nhìn (Scroll): Tự động cuộn trang cho đến khi bạn thấy được element đó.
- Chờ element nhận được sự kiện (Pointer Events): Đợi cho đến khi element không bị che khuất bởi một element khác (ví dụ: một pop-up, một modal) tại chính điểm sẽ click.
- Thử lại (Retry): Nếu element bị "gỡ" (detached) khỏi DOM trong bất kỳ bước nào, Playwright sẽ thử lại toàn bộ quy trình.
Chỉ khi TẤT CẢ các điều kiện này được đáp ứng, Playwright mới thực hiện hành động. Đây chính là lý do tại sao test của Playwright cực kỳ ổn định và bạn hiếm khi cần dùng waitForTimeout.
Các hành động Click cơ bản
Đây là các hành động click phổ biến nhất.
Click trái (Standard Click)
Hành động click() là một cú click chuột trái tiêu chuẩn.
// Click vào một Button
await page.getByRole('button', { name: 'Đăng nhập' }).click();
// Click vào một Link
await page.getByRole('link', { name: 'Xem chi tiết' }).click();
// Click vào một text cụ thể
await page.getByText('Bắt đầu').click();
Click đúp (Double Click)
Sử dụng dblclick() để mô phỏng một cú click đúp.
// Click đúp để mở một mục
await page.getByText('Item 1').dblclick();
// Click đúp để vào chế độ chỉnh sửa
await page.locator('#item-name').dblclick();
Click chuột phải (Right Click)
Sử dụng rightClick() (hoặc click({ button: 'right' })) để mở menu ngữ cảnh (context menu).
// Cách 1: Dùng alias (dễ đọc hơn)
await page.locator('#my-element').rightClick();
// Cách 2: Dùng options
await page.locator('#my-element').click({ button: 'right' });
// Ví dụ: Mở menu và chọn "Copy"
await page.locator('#file-explorer').rightClick();
await page.getByText('Copy').click(); // Menu ngữ cảnh xuất hiện
Click với các Tùy chọn (Options)
Bạn có thể tùy chỉnh hành vi click() bằng cách truyền vào một đối tượng options.
Click với Phím bổ trợ (Modifiers)
Mô phỏng việc người dùng giữ một phím trong khi click.
// Giữ phím Shift + Click (ví dụ: chọn nhiều mục trong danh sách)
await page.getByText('Mục 1').click();
await page.getByText('Mục 5').click({ modifiers: ['Shift'] });
// Giữ phím Control (hoặc Meta/Cmd trên Mac) + Click (mở link trong tab mới)
await page.getByRole('link', { name: 'Trang chủ' }).click({ modifiers: ['Control'] });
Các phím hỗ trợ: 'Shift', 'Control', 'Alt', 'Meta' (phím Command trên Mac, phím Windows trên PC).
Click vào Tọa độ (Position)
Mặc định, Playwright click vào trung tâm của element. Bạn có thể chỉ định chính xác vị trí (x, y) (tương đối so với góc trên-trái của element) để click.
Trị giá: Rất hữu ích khi click vào bản đồ (map), biểu đồ (chart), hoặc canvas.
// Click vào vị trí (x: 150, y: 100) bên trong element #heatmap
await page.locator('#heatmap').click({
position: { x: 150, y: 100 }
});
Forcing the Click (Bỏ qua Actionability) ⚠️
Đôi khi, các ứng dụng web có logic phức tạp, ví dụ khi bạn di chuột vào một nút, một element khác (như tooltip) lại xuất hiện và che mất cái nút đó. Playwright sẽ coi đây là lỗi (vì element bị che khuất) và không click được.
Nếu bạn chắc chắn rằng đây là hành vi đúng của ứng dụng (chứ không phải bug), bạn có thể bỏ qua tất cả các kiểm tra "Actionability" và ép buộc click.
// Bỏ qua mọi kiểm tra (visible, stable, obscured) và click ngay lập tức
await page.getByRole('button').click({ force: true });
CẢNH BÁO: Đây là một "anti-pattern" (cách làm không tốt).
Test của bạn không còn mô phỏng người dùng thật nữa.
Nó có thể che giấu các lỗi thật (ví dụ: element bị che khuất thật).
Hãy luôn luôn ưu tiên sửa locator hoặc đợi cho element (như pop-up) biến mất, và chỉ dùng force: true như là giải pháp cuối cùng.
Các Hành động Chuột Khác
Ngoài click, đây là các hành động quan trọng khác:
Di chuột (Hover)
Sử dụng hover() để mô phỏng việc di chuột vào một element.
Trị giá: Làm hiện các menu con (dropdowns) hoặc các tooltip.
// Di chuột vào menu "Sản phẩm"
await page.getByRole('button', { name: 'Sản phẩm' }).hover();
// Chờ menu con xuất hiện và click vào "Laptop"
await page.getByRole('link', { name: 'Laptop' }).click();
Kéo và Thả (Drag and Drop)
Playwright cung cấp một hàm cấp cao dragTo() cực kỳ mạnh mẽ.
const source = page.locator('#item-to-drag');
const target = page.locator('#drop-zone');
// Tự động kéo từ tâm 'source' đến tâm 'target'
await source.dragTo(target);
// Kiểm tra kết quả
await expect(target).toContainText('Đã thả!');
Hàm dragTo() này cũng tự động thực hiện toàn bộ các bước "Actionability" (tự cuộn, chờ, v.v.) cho cả hai element.
API Chuột Cấp thấp (Low-level)
Trong trường hợp dragTo() thất bại (ví dụ với các thư viện Kéo-Thả phức tạp), bạn có thể mô phỏng chính xác từng bước:
// 1. Di chuột đến element nguồn
await page.locator('#item-to-drag').hover();
// 2. Nhấn giữ chuột trái
await page.mouse.down();
// 3. Di chuột đến element đích
await page.locator('#drop-zone').hover();
// 4. Thả chuột
await page.mouse.up();
Phần 2: Mô phỏng Bàn phím (Keys & Shortcuts)
Playwright cung cấp phương thức locator.press() để mô phỏng chính xác một lần nhấn phím (keystroke) của người dùng.
Phương thức locator.press() sẽ tự động focus vào element được chọn và tạo ra một sự kiện gõ phím duy nhất. Nó chấp nhận các tên phím logic (giống như keyboardEvent.key trong JavaScript) hoặc các tổ hợp phím tắt.
Nhấn các Phím Chức năng
Đây là các phím không phải là ký tự, dùng để điều khiển hoặc kích hoạt hành động.
Các phím phổ biến: Enter, Tab, Escape, Backspace, Delete, ArrowLeft, ArrowRight, ArrowUp, ArrowDown, Home, End, PageUp, PageDown, F1...F12.
// Dùng 'Enter' để gửi (submit) một form
await page.getByText('Submit').press('Enter');
// Dùng 'Tab' để di chuyển focus sang element tiếp theo
await page.locator('#username').press('Tab');
// Dùng 'Escape' để đóng một hộp thoại (dialog)
await page.locator('.modal-content').press('Escape');
Nhấn các Ký tự Đơn
Bạn cũng có thể chỉ định một ký tự duy nhất mà bạn muốn gõ.
// Gõ ký tự "$"
await page.getByRole('textbox').press('$');
// Gõ chữ 'a' thường
await page.getByRole('textbox').press('a');
Nhấn Tổ hợp Phím (Shortcuts)
Đây là sức mạnh thực sự của press(). Bạn có thể mô phỏng các phím bổ trợ (modifiers) bằng cách kết hợp chúng với dấu +.
Các phím bổ trợ: Shift, Control (hoặc Ctrl), Alt, Meta (phím Command ⌘ trên Mac, phím Windows trên PC).
// Di chuyển con trỏ sang phải 1 từ (Ctrl + Mũi tên phải)
await page.getByRole('textbox').press('Control+ArrowRight');
// Chọn tất cả văn bản (Ctrl + A)
await page.getByRole('textbox').press('Control+A');
// Gõ chữ 'A' hoa
await page.getByRole('textbox').press('Shift+A');
// Lưu file (Cmd + S)
await page.locator('body').press('Meta+S');
So sánh: press() vs. fill() vs. pressSequentially()
Việc chọn đúng phương thức là rất quan trọng để test ổn định.
locator.fill(value) ⚡ (Nhanh & Ổn định)
Hành vi: Xóa (clear) ô input và điền ngay lập tức giá trị value vào.
Mô phỏng: Nó không mô phỏng từng lần gõ phím. Nó chỉ đặt giá trị (set value).
Khi nào dùng: Hầu hết mọi lúc (99%). Khi bạn chỉ quan tâm đến kết quả là ô input có text "Hello World", hãy dùng fill(). Đây là cách nhanh và đáng tin cậy nhất
// Cách tốt nhất để điền form
await page.locator('#email').fill('example@gmail.com');
locator.press(key) ⌨️ (Kiểm tra Sự kiện)
Hành vi: Mô phỏng một lần nhấn phím duy nhất (bao gồm keyDown, keyPress, keyUp).
Khi nào dùng: Khi bạn cần kiểm tra các sự kiện (event listeners) hoặc phím tắt.
Test các phím tắt (hotkeys) của ứng dụng (ví dụ: Control+S).
Test gửi form bằng phím Enter.
Test các ô tìm kiếm tự động (auto-suggest) mà kích hoạt sự kiện onKeyDown.
locator.pressSequentially(text) 🧑💻 (Mô phỏng người gõ)
Hành vi: Mô phỏng việc gõ từng ký tự một trong một chuỗi, giống hệt như người dùng thật.
Khi nào dùng: Khi fill() không hoạt động, thường là với các ô input "phức tạp" (ví dụ: các code editor, các ô input được "masked" - che giấu) mà dựa vào từng sự kiện gõ phím để định dạng text.
// Mô hỏng gõ chậm từng chữ "Hello"
await page.locator('input').pressSequentially('Hello');
// Thêm độ trễ 100ms giữa mỗi lần gõ
await page.locator('input').pressSequentially('Hello', { delay: 100 });
Focus Element (Tập trung vào Element)
Playwright cung cấp phương thức locator.focus() để mô phỏng việc "focus" (thường là dùng phím Tab hoặc click vào) một element.
// Tập trung vào một ô input
await page.locator('#my-input').focus();
Tại sao focus() ít khi được dùng?
Bạn sẽ thấy mình rất hiếm khi cần gọi locator.focus() một cách tường minh. Lý do là:
Hầu hết các hành động đã tự động focus() cho bạn!
Các hành động cấp cao (actionability) của Playwright như click(), fill(), press() đều tự động focus vào element trước khi thực hiện hành động.
// HÀNH ĐỘNG NÀY:
await page.locator('#my-input').fill('Hello');
// ĐÃ BAO GỒM:
// 1. Tự động cuộn đến #my-input
// 2. Tự động focus() vào #my-input
// 3. Tự động điền text
Khi nào thì focus() hữu ích?
Trường hợp sử dụng duy nhất của focus() là khi bạn muốn kiểm tra một hành vi chỉ xảy ra khi element nhận được focus (mà không có hành động nào khác theo sau).
Trị giá: Kiểm tra các sự kiện onFocus hoặc onBlur.
Ví dụ: Một ô input hiển thị một thông báo "hint" (gợi ý) đặc biệt ngay khi bạn focus vào nó.
const input = page.locator('#password');
const hint = page.locator('#password-hint');
// Ban đầu, hint bị ẩn
await expect(hint).not.toBeVisible();
// 1. Chỉ focus vào ô input
await input.focus();
// 2. Kiểm tra xem hint đã xuất hiện chưa
await expect(hint).toBeVisible();
await expect(hint).toContainText('Mật khẩu phải có ít nhất 8 ký tự');
