NỘI DUNG BÀI HỌC

✅ Củng cố mô hình Arrange – Act – Assert (AAA) và sử dụng expect trong Playwright.
✅ Thành thạo Fixtures trong pytest (cơ bản, nâng cao, nhiều scope) để tái sử dụng code setup & quản lý môi trường test.
✅ Hiểu và áp dụng Multi Tabs: mở tab mới từ click, chờ tab mới bằng context.expect_page, xây BasePage hỗ trợ.
✅ Làm chủ thao tác người dùng nâng cao:

  • Upload File với set_input_files()

  • Keyboard Actions với page.keyboard.press()

✅ Hoàn thiện kịch bản E2E tổng hợp trên hệ thống HRM kết hợp AAA + Fixtures + Multi Tabs + Upload + Keyboard.



1️⃣ MÔ HÌNH AAA & EXPECT – VIẾT TEST CÓ TƯ DUY


1.1. Khái niệm AAA

Trong mọi test, luôn cố gắng chia rõ 3 phần:

  • Arrange (Chuẩn bị)
    Chuẩn bị mọi thứ cần thiết trước khi hành động:

    • Mở trang

    • Login

    • Tạo dữ liệu

    • Khởi tạo Page Object / Fixture

  • Act (Hành động)
    Thao tác chính mà ta muốn kiểm thử:

    • Click nút

    • Nhập form

    • Chọn dropdown

    • Upload file

  • Assert (Kiểm chứng)
    Kiểm tra kết quả, dùng expect trong Playwright:

    • expect(page).to_have_url(...)

    • expect(locator).to_be_visible()

    • expect(locator).to_have_text(...)

Lý do dùng AAA:

  • Code test dễ đọc: nhìn vào biết test đang làm gì.

  • Dễ review: biết chỗ nào là setup, chỗ nào là logic chính.

  • Dễ debug: nếu lỗi, biết lỗi ở Arrange/Act/Assert.


1.2. Ví dụ: Login thành công (AAA + expect)

 
from playwright.sync_api import Page, expect

def test_login_success(page: Page):
    # ARRANGE – Chuẩn bị
    page.goto("https://hrm.anhtester.com/erp/login")

    # ACT – Hành động
    page.fill("#iusername", "admin_example")
    page.fill("#ipassword", "123456")
    page.click("button[type='submit']")

    # ASSERT – Kiểm chứng
    expect(page).to_have_url("**/erp/dashboard")
    expect(page.locator("h4.page-title")).to_have_text("Dashboard")


1.3. Bài tập 1 – AAA với login thất bại

Viết test test_login_fail_with_wrong_password theo AAA:

  • Arrange:

    • Mở trang login HRM

  • Act:

    • Nhập username đúng

    • Nhập password sai

    • Click Login

  • Assert:

    • Không chuyển sang /dashboard

    • Có hiển thị báo lỗi (VD: “Invalid credentials”)

Gợi ý cấu trúc:

def test_login_fail_with_wrong_password(page: Page):
    # Arrange

    # Act

    # Assert
 

2️⃣ FIXTURES – QUẢN LÝ MÔI TRƯỜNG & LOGIN DÙNG CHUNG


2.1. Fixtures là gì? Dùng để làm gì?

Trong pytest, fixture là nơi ta:

  • Setup môi trường test:

    • Mở browser, mở page

    • Login

    • Tạo dữ liệu test

  • (Tuỳ trường hợp) Teardown:

    • Xoá dữ liệu

    • Đóng browser / cleanup

Mục tiêu:

  • Không lặp đi lặp lại code login trong từng test.

  • Tách phần “Chuẩn bị” (Arrange) ra khỏi test → test ngắn & dễ hiểu.


2.2. Ví dụ: Fixture cho LoginPage

Giả sử ta đã có class LoginPage:

 
# pages/login_page.py
from core.base_page import BasePage
from playwright.sync_api import Page, expect

class LoginPage(BasePage):
    URL = "https://hrm.anhtester.com/erp/login"
    USERNAME_INPUT = "#iusername"
    PASSWORD_INPUT = "#ipassword"
    LOGIN_BUTTON   = "button[type='submit']"

    def open(self):
        self._open_url(self.URL)

    def login(self, username: str, password: str):
        self._fill(self.USERNAME_INPUT, username)
        self._fill(self.PASSWORD_INPUT, password)
        self._click(self.LOGIN_BUTTON)

    def assert_login_success(self):
        expect(self.page).to_have_url("**/erp/dashboard")

Tạo fixture trong conftest.py:

# conftest.py
import pytest
from pages.login_page import LoginPage

@pytest.fixture
def login_page(page):
    return LoginPage(page)

Dùng trong test:

 
def test_login_success_with_fixture(login_page):
    # Arrange
    login_page.open()

    # Act
    login_page.login("admin_example", "123456")

    # Assert
    login_page.assert_login_success()
​

👉 Ưu điểm:

  • Test không cần biết chi tiết locator


2.3. Fixture nâng cao – Dùng yield để setup + teardown

Khi cần vừa setup – vừa dọn dẹp, dùng yield:

import pytest

@pytest.fixture
def temp_employee(page):
    # Setup – tạo employee
    page.goto("https://hrm.anhtester.com/erp/employees")
    page.click("button:has-text('Add Employee')")
    page.fill("#employee_name", "Temp User")
    page.click("button:has-text('Save')")

    # Trả về dữ liệu cho test dùng
    yield "Temp User"

    # Teardown – xoá employee sau khi test xong
    page.goto("https://hrm.anhtester.com/erp/employees")
    page.fill("#search_employee", "Temp User")
    page.click("button:has-text('Delete')")

Test có thể dùng temp_employee:

def test_view_temp_employee(temp_employee, page):
    page.goto("https://hrm.anhtester.com/erp/employees")
    page.fill("#search_employee", temp_employee)
    page.click("button:has-text('Search')")
    # Assert gì đó...


2.4. Scope fixture – function / module / session

@pytest.fixture(scope="function")
def login_per_test(...):
    ...

@pytest.fixture(scope="module")
def login_per_module(...):
    ...

@pytest.fixture(scope="session")
def login_per_session(...):
    ...
  • function

    • Mặc định

    • Mỗi test chạy lại fixture từ đầu

    • Cô lập, ít side-effect

  • module

    • 1 file test dùng chung 1 lần setup

    • Tiết kiệm thời gian khi nhiều test dùng chung login

  • session

    • Toàn bộ test run dùng 1 lần fixture

    • Rất nhanh nhưng dễ bị ảnh hưởng lẫn nhau nếu code test chỉnh state quá nhiều


2.5. Ứng dụng: Fixture trả về DashboardPage đã login

 
# conftest.py
import pytest
from pages.login_page import LoginPage
from pages.dashboard_page import DashboardPage

@pytest.fixture
def dashboard_page(page):
    login_page = LoginPage(page)

    # Arrange – login sẵn
    login_page.open()
    login_page.login("admin_example", "123456")

    dashboard = DashboardPage(page)
    dashboard.assert_is_current_page()

    return dashboard

Giờ test chỉ cần:

def test_dashboard_title(dashboard_page):
    dashboard_page.assert_is_current_page()
    # Có thể thêm assert khác về menu, header, v.v.


2.6. Bài tập 2 – Xây fixture login dùng chung

Yêu cầu:

  1. Tạo fixture dashboard_page:

    • Mở trang login

    • Login bằng tài khoản admin_example/123456

    • Khởi tạo DashboardPage

    • Gọi dashboard.assert_is_current_page()

    • Return dashboard

  2. Viết 2 test:

    • Test 1: Kiểm tra menu “PIM” hiển thị

    • Test 2: Kiểm tra menu “Time” hiển thị

Gợi ý:
Các method có thể đặt trong DashboardPage:

  • is_menu_visible("PIM")

  • is_menu_visible("Time")


3️⃣ MULTI TABS – MỞ TAB MỚI TỪ CLICK

Phần này là ôn lại + nâng cấp từ giáo án trước:
Học viên nào bỏ lỡ buổi Multi Tabs sẽ được thấy ứng dụng thực tế trong buổi tổng hợp.


3.1. Khi nào cần xử lý Multi Tabs?

Một số tình huống:

  • Click vào link “Settings”, “Help”, “Policy” mở tab mới.

  • Link có target="_blank".

  • Website mở popup/tap mới bằng window.open().

Nếu không xử lý đúng, test sẽ:

  • ❌ Vẫn đứng ở tab cũ

  • ❌ Không biết tab mới đã xuất hiện

  • ❌ Click / assert ở “sai tab” → locator không tìm thấy


3.2. Nguyên tắc vàng: Lắng nghe trước – Click sau

Playwright cung cấp:

 
context.expect_page()
​

 

Quy tắc:

  1. Gọi context.expect_page() ở dạng with → chờ tab mới xuất hiện.

  2. Bên trong block đó mới click vào link mở tab.

  3. Sau khi ra khỏi with, lấy new_page = new_page_info.value.

Ví dụ:

with page.context.expect_page() as new_page_info:
    page.click("a[target='_blank']")

new_page = new_page_info.value
new_page.wait_for_load_state("load")

Nếu làm ngược:

  • Click xong

  • Rồi mới expect_page → lỡ mất event mở tab → code bị treo hoặc fail.


3.3. BasePage hỗ trợ Multi Tabs

Để tái sử dụng, ta viết helper trong BasePage:

# core/base_page.py
from playwright.sync_api import Page

class BasePage:
    def __init__(self, page: Page):
        self.page = page

    def _click_and_wait_for_new_page(self, locator: str, name: str = "", timeout: int = 15000):
        """
        Click vào locator → mở tab mới → return Page mới.
        """
        print(f"[MultiTab]: Click '{name}' và chờ tab mới mở...")

        with self.page.context.expect_page(timeout=timeout) as new_page_info:
            self.page.locator(locator).click()

        new_page = new_page_info.value
        new_page.wait_for_load_state("load")

        print(f"[MultiTab]: Tab mới URL = {new_page.url}")
        return new_page
 

3.4. DashboardPage mở Settings ở tab mới

 
# pages/dashboard_page.py
from core.base_page import BasePage
from pages.settings_page import SettingsPage
from playwright.sync_api import expect

class DashboardPage(BasePage):

    SETTINGS_LINK = "a[href='/erp/settings/general']"
    URL_FRAGMENT = "/erp/dashboard"

    def assert_is_current_page(self):
        self.page.wait_for_url(f"**{self.URL_FRAGMENT}")
        expect(self.page.locator("h4.page-title")).to_have_text("Dashboard")

    def open_settings_in_new_tab(self) -> SettingsPage:
        new_page = self._click_and_wait_for_new_page(
            self.SETTINGS_LINK,
            name="Mở trang Cài đặt"
        )
        return SettingsPage(new_page)


3.5. SettingsPage trên tab mới

 
# pages/settings_page.py
from core.base_page import BasePage
from playwright.sync_api import expect

class SettingsPage(BasePage):

    URL_FRAGMENT = "/erp/settings/general"
    HEADER = "h4:has-text('General Settings')"

    def assert_is_current_page(self):
        self.page.wait_for_url(f"**{self.URL_FRAGMENT}")
        expect(self.page.locator(self.HEADER)).to_be_visible()

    def update_company_name(self, new_name: str):
        self.page.fill("input[name='company_name']", new_name)
        self.page.click("button:has-text('Save')")

    def close_tab(self):
        self.page.close()


3.6. Ví dụ test Multi Tab đầy đủ

# tests/test_multi_tab_settings.py
from playwright.sync_api import expect

def test_settings_tab_flow(dashboard_page):
    # Arrange
    dashboard_page.assert_is_current_page()

    # Act – Mở tab mới và thao tác
    settings_page = dashboard_page.open_settings_in_new_tab()
    settings_page.assert_is_current_page()
    settings_page.update_company_name("AnhTester Academy")
    settings_page.close_tab()

    # Assert – Quay về tab cũ
    dashboard_page.assert_is_current_page()


3.7. Bài tập 3 – Tự xử lý 1 tab mới

Yêu cầu:

  • Tìm hoặc mô phỏng 1 link trong HRM/website demo có mở tab mới.

  • Dùng helper _click_and_wait_for_new_page trong BasePage.

  • Viết test:

    1. Đứng ở trang A (dashboard).

    2. Click mở trang B (settings/help) trên tab mới.

    3. Assert heading/URL tab B.

    4. Đóng tab B.

    5. Quay lại tab A → assert vẫn đang ở trang A.


4️⃣ UPLOAD FILE – set_input_files()


4.1. Khái niệm & Lưu ý

Upload file trong Playwright:

  • Không cần click nút “Choose file”.

  • Chỉ cần tìm đúng <input type="file">.

  • Dùng locator.set_input_files("path/to/file").

Lưu ý:

  • Path file là path từ thư mục chạy pytest (thường là root project).

  • File upload nên để trong thư mục data/ hoặc resources/.


4.2. Ví dụ: Upload avatar trên trang Profile

Giả sử có ProfilePage:

 
# pages/profile_page.py
from core.base_page import BasePage
from playwright.sync_api import expect

class ProfilePage(BasePage):

    AVATAR_INPUT = "input[type='file']"
    UPLOAD_BUTTON = "button:has-text('Upload')"
    SUCCESS_TOAST = ".toast-success"

    def open(self):
        self._open_url("https://hrm.anhtester.com/erp/profile")

    def upload_avatar(self, file_path: str):
        self.page.set_input_files(self.AVATAR_INPUT, file_path)
        self.page.click(self.UPLOAD_BUTTON)

    def assert_upload_success(self):
        expect(self.page.locator(self.SUCCESS_TOAST)).to_be_visible()
        expect(self.page.locator(self.SUCCESS_TOAST)).to_have_text("Upload successful")

Test:

def test_upload_avatar_success(dashboard_page):
    # Arrange
    profile_page = dashboard_page.open_profile_page()  # hoặc profile_page = ProfilePage(dashboard_page.page)
    profile_page.open()

    # Act
    profile_page.upload_avatar("data/avatar.png")

    # Assert
    profile_page.assert_upload_success()


4.3. Bài tập 4 – Upload logo công ty

Yêu cầu:

  • Tạo method upload_company_logo(file_path) trong SettingsPage.

  • Viết test:

    1. Dùng dashboard_page

    2. Mở SettingsPage

    3. Upload logo data/company_logo.png

    4. Assert:

      • Toast success hiển thị

      • Hoặc kiểm tra logo mới xuất hiện (tuỳ UI demo)


5️⃣ KEYBOARD ACTIONS – page.keyboard.press()


5.1. Khi nào cần dùng bàn phím?

  • Submit form bằng Enter.

  • Dùng phím mũi tên để chọn item trong dropdown.

  • Ctrl+A, Delete để xoá input.


5.2. Ví dụ: Submit login bằng phím Enter

 
from playwright.sync_api import Page, expect

def test_login_with_enter_key(page: Page):
    # Arrange
    page.goto("https://hrm.anhtester.com/erp/login")
    page.fill("#iusername", "admin_example")
    page.fill("#ipassword", "123456")

    # Act – dùng phím Enter thay vì click
    page.keyboard.press("Enter")

    # Assert
    expect(page).to_have_url("**/erp/dashboard")


5.3. Ví dụ: Xoá nội dung bằng Ctrl+A + Delete

 
def test_clear_input_with_keyboard(page: Page):
    # Arrange
    page.goto("https://example.com/form")
    page.fill("#full_name", "Automation Testing")

    # Act – chọn hết & xoá
    page.click("#full_name")
    page.keyboard.press("Control+A")
    page.keyboard.press("Delete")

    # Assert – input trống
    expect(page.locator("#full_name")).to_have_value("")


5.4. Bài tập 5 – Keyboard thực tế

Yêu cầu:

  • Chọn 1 form có ô search hoặc ô text.

  • Flow:

    1. Điền 1 chuỗi text

    2. Dùng Control+A + Delete để xoá

    3. Assert ô input rỗng

  • Sau đó, nhập text mới, bấm Enter để submit, assert chuyển trang hoặc hiển thị kết quả.


6️⃣ BÀI TẬP TỔNG HỢP CUỐI BUỔI (CHALLENGE E2E)


6.1. Mục tiêu

Viết 1 test E2E kết hợp toàn bộ kiến thức buổi 9:

  • AAA

  • Fixtures (dashboard_page)

  • Multi Tabs (Dashboard → Settings)

  • Upload File (logo hoặc avatar)

  • Keyboard (Enter / Ctrl+A + gõ lại)


6.2. Đề bài gợi ý

Scenario: Cập nhật thông tin công ty & logo qua trang Settings

  1. Arrange:

    • Dùng fixture dashboard_page (đã login).

    • Mở SettingsPage trong tab mới (multi tab).

  2. Act:

    • Trong SettingsPage:

      • Dùng Ctrl+A + Delete để xoá tên công ty cũ, nhập tên mới.

      • Upload logo mới bằng set_input_files().

      • Bấm Enter (hoặc click Save).

  3. Assert:

    • Assert toast “Update successful”.

    • Đóng tab Settings.

    • Quay về Dashboard + assert vẫn đang ở Dashboard.


6.3. Gợi ý code cấu trúc

# tests/test_e2e_update_company_settings.py
import pytest
from playwright.sync_api import expect
from pages.settings_page import SettingsPage

@pytest.mark.e2e
def test_e2e_update_company_settings(dashboard_page):
    # ARRANGE
    dashboard_page.assert_is_current_page()

    settings_page = dashboard_page.open_settings_in_new_tab()
    settings_page.assert_is_current_page()

    # ACT – cập nhật tên & upload logo
    settings_page.clear_and_set_company_name("AnhTester Academy")
    settings_page.upload_company_logo("data/company_logo.png")
    settings_page.submit_with_enter_key()

    # ASSERT – cập nhật thành công
    settings_page.assert_update_success()
    settings_page.close_tab()

    # Quay về dashboard
    dashboard_page.assert_is_current_page()

Trong SettingsPage, ta có thể viết:

def clear_and_set_company_name(self, new_name: str):
    name_input = self.page.locator("input[name='company_name']")
    name_input.click()
    self.page.keyboard.press("Control+A")
    self.page.keyboard.press("Delete")
    name_input.fill(new_name)

def upload_company_logo(self, file_path: str):
    self.page.set_input_files("input[type='file']", file_path)

def submit_with_enter_key(self):
    self.page.keyboard.press("Enter")

def assert_update_success(self):
    toast = self.page.locator(".toast-success")
    expect(toast).to_be_visible()
    expect(toast).to_have_text("Update successful")


7️⃣ TÓM TẮT KIẾN THỨC CỐT LÕI BUỔI 9

Mảng Cần hiểu Công cụ / Kỹ thuật Ý nghĩa
AAA Chia test thành 3 bước Arrange – Act – Assert Test rõ ràng, dễ review
Fixtures Chuẩn bị state dùng chung @pytest.fixture, yield, scope Login 1 lần, đỡ lặp code
Multi Tabs Tab mới là Page mới context.expect_page(), helper _click_and_wait_for_new_page Điều khiển được tab mới
Upload File Mô phỏng người dùng chọn file locator.set_input_files() Test upload không phụ thuộc UI “Choose file”
Keyboard Điều khiển bằng phím page.keyboard.press() Xử lý Enter, Ctrl+A, Delete trong form

Teacher

Teacher

Hà Lan

QA Automation

With over 5 years of experience in web, API, and mobile test automation, built strong expertise in designing and maintaining automation frameworks across various domains and international projects. Committed to mentoring and knowledge sharing, I provide practical guidance and proven techniques to help aspiring testers develop their skills and succeed in the automation field.

Cộng đồng Automation Testing Việt Nam:

🌱 Telegram Automation Testing:   Cộng đồng Automation Testing
🌱 
Facebook Group Automation: Cộng đồng Automation Testing Việt Nam
🌱 
Facebook Fanpage: Cộng đồng Automation Testing Việt Nam - Selenium
🌱 Telegram
Manual Testing:   Cộng đồng Manual Testing
🌱 
Facebook Group Manual: Cộng đồng Manual Testing Việt Nam

Chia sẻ khóa học lên trang

Bạn có thể đăng khóa học của chính bạn lên trang Anh Tester để kiếm tiền

Danh sách bài học