Web Automation
Duy Văn | 10/02/2025
Về cơ bản thì cách viết test automation cho web app với Playwright không khác gì mấy so với Selenium, chỉ khác ở cách Playwright hoạt động
Browsers, contexts và pages
Như đã đề cập, Playwright hơn Selenium ở chỗ trình duyệt của Playwright hoạt động cho phép nhiều page - multi-context trên cùng 1 trình duyệt
1 trình duyệt là 1 instance của trình duyệt web, ví dụ như nếu ta đang nói về Chrome, thì 1 trình duyệt có thể nhìn thấy là 1 cửa sổ Chrome, và mỗi cửa sổ Chrome có thể mở nhiều tab, mỗi tab là 1 page
Trình duyệt này sẽ có context, context là một session cô lập gần giống như chế độ ẩn danh (incognito), context này có thể chia sẻ giữa các trang web / các bài test với nhau hoặc riêng biệt hoàn toàn, practice cơ bản đó là tất cả các bài test sẽ dùng chung 1 trình duyệt nhưng từng test sẽ có context riêng
1 page là 1 tab trình duyệt, 1 context có thể có nhiều pages.

Minh hoạ Browser, Context và Page
Triển khai Playwright
Thông thường, khi thực hiện test dùng Playwright Python thì chúng ta sẽ cần gọi nó ra như lúc mở file
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
context = browser.new_context()
page = context.new_page()
Ở đây sync_playwright là một dạng context manager, nó sẽ tự động đóng browser với browser.close() sau khi kết thúc block code
Tuy nhiên, nếu tích hợp sử dụng pytest với pytest-playwright, thì chúng ta sẽ không cần phải viết như vậy, do pytest-playwright sẽ tự động quản lý fixture browser, context và page
Fixture browser sẽ cung cấp một instance của trình duyệt mở bởi Playwright, ví dụ như muốn custom cái trình duyệt này, ví dụ như là proxy thì chúng ta sẽ overwrite fixture, nó sẽ trả ra một Dict chứa các thông số cần thiết cho trình duyệt
@pytest.fixture(scope="session")
def browser_type_launch_args(browser_type_launch_args):
return {
**browser_type_launch_args,
"proxy": {
"server": "http://myproxy.com:3128",
"username": "usr",
"password": "pwd"
}
}
Fixture context cung cấp một context trình duyệt mới, cũng có thể được custom như trên bằng cách overwrite fixture
@pytest.fixture
def browser_context_args(browser_context_args):
return {
**browser_context_args,
"viewport": {"width": 1280, "height": 720},
"user_agent": "MyCustomUserAgent/1.0",
"locale": "en-US"
}
Fixture page cung cấp một page mới, mỗi khi cần viết test, ví dụ như lên page này, ấn cái này cái kia thì fixture Page sẽ là cái luôn luôn cần thiết
Bắt đầu với import Page từ playwright.sync_api và viết test gọi ra param và type hint cho nó thành class Page
from playwright.sync_api import Page
def test_page(page: Page) -> None:
# ...
Bây giờ khi bài test đã được dựng, nó có thể truy cập vào một trang web và có context riêng. Nếu chúng ta viết nhiều bài test thì mỗi test sẽ có một trang và context riêng, nhưng sẽ luôn ở trong cùng một trình duyệt
Ví dụ
Để bắt đầu bài test thì trước hết với một bài test đơn giản là lên web và kiểm tra tittle của trang
from playwright.sync_api import Page
import re
def test_has_title(page: Page):
page.goto("https://www.google.com/", wait_until='networkidle')
assert re.match(r"Google", page.title())
Bài test trên lên trang Google, đợi đến khi sự kiện networkidle diễn ra và kết thúc, sau đó mới tiếp tục với assert - kiểm tra tên của page có từ Google hay không
Với bài test cơ bản như thế này thì chưa cần pytest, chúng ta chỉ cần pytest khi thực hiện nhiều test, ví dụ, nếu chúng ta có 3 bài test, lên Google, Facebook, Youtube và đều cẩn sử dụng chung một tài khoản email để đăng nhập thì chúng ta sẽ cần pytest để quản lý các bài test này
Ngoài ra, khi thực hiện test, param wait_until có thể được sử dụng với các arg sau:
| ‘load’ | Đợi event load khởi chạy, có nghĩa là |
| ‘domcontentloaded’ | Đợi event DOMContentLoaded |
| ‘networkidle’ | Đợi đến khi không còn kết nối nào trong ít nhất 500 ms - cách này nên tránh sử dụng do cách đợi 0.5s không phải lúc nào cũng đúng và không nên dùng làm căn cứ xác định trang đã load xong và sẵn sàng tương tác |
| ‘commit’ | Đợi đến khi phản hồi được nhận và trang bắt đầu tải |
Đồng thời, có thể sử dụng argument timeout để đặt thời gian chờ, mặc định 30s nếu không được đặt
Assertion
Khi thực hiện các bước assert web để xác minh trang web đã sẵn sàng tương tác hay chưa thì có thể sử dụng “explicit assertion” bằng cách áp dụng expect từ playwright.sync_api
from playwright.sync_api import Page, expect
def test_has_title(page: Page):
page.goto("https://www.google.com/")
expect(page).to_have_title("Google")
Thời gian mặc định để expect đánh fail là 5s. Cách đặt timeout thì có thể đặt expect.set_options(timeout=10000) để đặt timeout 10s
Ngoài ra, các function expect có thể xem ở đây
Action
Các hành động trong Playwright cơ bản cũng bao gồm các hành động như click, type, hover, check, uncheck, select, … Và đều được bắt theo element, để xác định element, mình viết bài mở rộng ở đây
Text input
Để điền text, chúng ta sử dụng method fill đi với locator
locator = page.locator('input')
locator.fill('Hello, World!')
Nó sẽ hoạt động cho 2 thẻ input và textarea
Đánh checkbox và radio buttons
Để đánh checkbox, chúng ta sử dụng method set_checked() đi với locator
locator = page.locator('input[type="checkbox"]')
locator.set_checked()
expect(locator).to_be_checked()
Nó sẽ hoạt động cho cả radio buttons, cụ thể là cho 2 thẻ input[type=checkbox] và input[type=radio]
Select
Đối với thẻ select thì chúng ta sử dụng method select_option() đi với locator
locator = page.locator('select')
locator.select_option('option1')
Chúng ta có thẻ truyền vào hoặc value hoặc label làm arg
Click
Để click vào element, chúng ta sử dụng một số method như click() đi với locator
locator = page.locator('button')
locator.click() #single
locator.dblclick() #double
locator.click(button='right') #right click
locator.click(modifiers = ['Shift']) #click with Shift key
locator.hover() #hover above
locator.click(position = {'x': 10, 'y': 10}) #click at position
Nhớ rằng toạ độ x và y của cick theo vị trí là toạ độ với gốc là góc trên cùng bên trái của element
Đôi khi trang web rất củ chuối khi cho gì đó hiện lên overlay và ngăn chúng ta click trực tiếp, khi sử dụng Selenium thì vấn đề này thường rất đau đầu. Playwright cho một giải pháp khá đơn giản, chúng ta có thể sử dụng force để bắt buộc click
locator = page.locator('button')
locator.click(force=True)
Ngoài ra, một phương pháp ép click đến từ HTML cũng có thể sử dụng, đó là dispatch_event đi với locator
locator = page.locator('button')
locator.dispatch_event('click')
Nhưng phương pháp này bỏ qua hoàn toàn hoạt động click chuột như người dùng web bình thường, thay vào đó là gửi một sự kiện click đến element
Type
Trong tình huống chúng ta không có textarea hay input nhưng web app của chúng ta là dạng interactive, cho phép bấm các nút tắt, thì việc mô phỏng nó với press_sequentially sẽ giải quyết được
locator = page.locator('body')
locator.press_sequentially(['Shift', 'KeyA', 'KeyB'])
Các event đi kèm sẽ đủ keydown, keypress, keyup event
Keys
Chúng ta cũng có thể thực hiện ấn các nút bàn phím với press
locator = page.locator('input')
locator.press('Enter')
Theo doc Playwright, các key được sử dụng cần theo đúng tên, bao gồm
Backquote, Minus, Equal, Backslash, Backspace, Tab, Delete, Escape,
ArrowDown, End, Enter, Home, Insert, PageDown, PageUp, ArrowRight,
ArrowUp, F1 - F12, Digit0 - Digit9, KeyA - KeyZ, etc.
Tải file lên
Để upload một file, thì chúng ta cần tương tác với các thẻ input[type=file], chúng ta sử dụng method set_input_files đi với locator
locator = page.locator('input[type=file]')
locator.set_input_files('file.txt')
Thường thì chúng ta sẽ phải truyền vào đường dẫn
locator.set_input_files('C:/Users/duyvan/Desktop/file.txt')
Nếu muốn bỏ đi file hiện tại, chúng ta sử dụng arg là một list trống
locator.set_input_files([])
Drag and Drop
Để thực hiện drag and drop, chúng ta sử dụng method drag_to đi với locator
source = page.locator('source')
target = page.locator('target')
source.drag_to(target)
Scroll
Thật ra Playwright đa số sẽ tự scroll tới element đang được tương tác
Nhưng nếu cần scroll tới vô hạn, chúng ta sử dụng method scroll_into_view_if_needed đi với locator
locator = page.locator('element')
locator.scroll_into_view_if_needed()
locator được scroll tới vị trí mà nó có thể nhìn thấy được và nằm gọn trong viewport (nếu có thể)
Alerts và Dialogs
Để xử lý các alert và dialog, cơ bản chúng ta chỉ cần quan tâm tới method accept và dismiss đi với page
page.on("dialog", lambda dialog: dialog.accept())
#page.on('dialog').dismiss()
locator = page.locator('button') #button tạo ra dialog
locator.click()
Tuy nhiên Playwright sẽ thường tự động dismiss dialogs, nhưng chúng ta có thể đánh handler trước hành động tạo ra dialogs để cụ thể hành động
Ví dụ với dialog dạng prompt
page.on('dialog', lambda dialog: dialog.accept('my-password'))
locator = page.locator('button') #button tạo ra dialog
locator.click()
iFrame
Để chuyển vào iFrame, chúng ta làm tương tự như bên Selenium
page.frame(name = 'frame-name') # theo tên
page.frame(index = 0) # theo index
page.frame(url = 'https://example.com') # theo url
Switching context
Trong Playwright, tất cả các context đều active (các trang đều cùng lúc active) và không cần chuyển qua lại, vấn đề là cần chọn đúng page object để thực hiện hành động
Ví dụ bài test sau sẽ mở ra 2 tab và thực hiện hành động trên tab thứ 2
def test_tabs(page: Page):
page.goto("https://www.google.com/")
page.evaluate("window.open('https://www.bing.com/')")
page.wait_for_timeout(5000)
pages = page.context.pages
assert len(pages) == 2
pages[1].goto("https://www.bing.com/")
Bài test trên thực hiện:
- Mở trang Google
- Mở trang Bing
- Chờ 5s
- Kiểm tra xem có 2 tab không
- Chuyển qua tab thứ 2
- Mở trang Bing
Dòng pages = page.context.pages sẽ trả ra một list các page trong context, từ đó chúng ta có thể “chuyển” (thực tế là không cần) , hay xác định chính xác page nào để tương tác