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