Page Object Model ở bài trước gom mọi thứ thuộc một màn hình về đúng page. Nhưng ứng dụng thật thường có toolbar xuất hiện ở mười màn hình, dialog xác nhận dùng ở năm flow và một danh sách gồm hai chục row có cùng cấu trúc. Nếu copy chúng vào từng Page Object thì chúng ta chỉ chuyển code trùng từ test sang resource, chưa giải quyết xong.
Component Object Model đi thêm một tầng: phần UI nào có cấu trúc và hành vi riêng, được tái sử dụng hoặc lặp lại, thì cho nó một resource riêng.
Đây là convention tổ chức code, không phải feature có nút bật trong Appium và cũng không phải model chính thức Robot Framework bắt bạn tuân theo. Nếu project có ba màn hình bé xíu mà dựng mười bảy component thì đấy là vẽ rắn thêm chân, không phải clean architecture.

Sơ đồ mình dựng để chỉ ranh giới trách nhiệm: page làm composition, component sở hữu locator và hành vi của phần UI có thể tái sử dụng.
Page khác Component ở điểm nào?
| Object | Đại diện cho | Sở hữu | Ví dụ |
|---|---|---|---|
| Page Object | Một màn hình hoặc trạng thái điều hướng chính | Locator cấp màn hình, hành vi và orchestration | Home Page, Search Page |
| Component Object | Một vùng UI có cấu trúc/hành vi riêng | Root, locator con và thao tác của component | Toolbar, menu row, dialog |
| Common keyword | Một thao tác kỹ thuật không thuộc UI cụ thể | Utility nhỏ, không giữ locator màn hình | Tạo tên screenshot, chuẩn hóa text |
Component không thay thế page. Page dùng component để hoàn thành hành vi của màn hình.
Cấu trúc project mở rộng
resources/
├── app.resource
├── components/
│ ├── app_bar.resource
│ ├── menu_item.resource
│ └── dialog.resource
└── pages/
├── home_page.resource
└── search_page.resource
components không phải chỗ quăng mọi keyword chưa biết để đâu. Một component phải trả lời được ba câu:
- Nó đại diện cho vùng UI nào?
- Root hoặc ranh giới của nó nằm ở đâu?
- Page nào đang sử dụng hành vi của nó?
Không trả lời được thì nhiều khả năng bạn đang tạo common.resource phiên bản đổi tên.
Menu item: component động theo label
Trong ApiDemos, các menu item có thể được tìm bằng accessibility id. Tạo resources/components/menu_item.resource:
*** Settings ***
Library AppiumLibrary
*** Keywords ***
Menu Item "${label}" Phải Hiển Thị
${locator}= Tạo Locator Cho Menu Item ${label}
Wait Until Page Contains Element ${locator} timeout=10s
Chọn Menu Item "${label}"
${locator}= Tạo Locator Cho Menu Item ${label}
Wait Until Page Contains Element ${locator} timeout=10s
Click Element ${locator}
Tạo Locator Cho Menu Item
[Arguments] ${label}
${locator}= Set Variable accessibility_id=${label}
RETURN ${locator}
Chọn Menu Item "App" là embedded argument: Robot Framework lấy phần nằm giữa hai dấu quote làm ${label}. Test hoặc page gọi rất tự nhiên:
Chọn Menu Item "App"
Chọn Menu Item "Search"
Keyword tạo locator được gom riêng để việc hiển thị và click không tự lắp chuỗi theo hai cách khác nhau. Nếu sau này menu chuyển sang resource-id, sửa một chỗ.
Vì ${label} đi thẳng vào accessibility id nên không có bài toán escape XPath. Nếu bạn dựng XPath động từ input bất kỳ, phải xử lý dấu quote và không cho test data tùy tiện biến thành biểu thức selector.
App bar: một component có locator cố định
Tạo resources/components/app_bar.resource:
*** Settings ***
Library AppiumLibrary
*** Variables ***
${APP_BAR_ROOT} id=android:id/action_bar_container
${APP_BAR_TITLE} id=android:id/action_bar_title
${APP_BAR_NAVIGATE_UP} accessibility_id=Navigate up
*** Keywords ***
App Bar Phải Có Tiêu Đề
[Arguments] ${expected_title}
Wait Until Page Contains Element ${APP_BAR_ROOT} timeout=10s
Element Text Should Be ${APP_BAR_TITLE} ${expected_title}
Điều Hướng Lùi Bằng App Bar
Wait Until Element Is Visible ${APP_BAR_NAVIGATE_UP} timeout=10s
Click Element ${APP_BAR_NAVIGATE_UP}
Root ${APP_BAR_ROOT} giúp xác nhận cả component đã xuất hiện trước khi đọc title. Nếu ứng dụng dùng toolbar tùy biến không có android:id/action_bar_*, component vẫn giữ nguyên API keyword, chỉ thay locator nội bộ.
Dialog: component sở hữu title, message và action
Tạo resources/components/dialog.resource:
*** Settings ***
Library AppiumLibrary
*** Variables ***
${DIALOG_ROOT} id=android:id/parentPanel
${DIALOG_TITLE} id=android:id/alertTitle
${DIALOG_MESSAGE} id=android:id/message
${DIALOG_POSITIVE} id=android:id/button1
${DIALOG_NEGATIVE} id=android:id/button2
*** Keywords ***
Dialog Phải Hiển Thị
Wait Until Element Is Visible ${DIALOG_ROOT} timeout=10s
Dialog Phải Có Nội Dung
[Arguments] ${expected_title} ${expected_message}
Dialog Phải Hiển Thị
Element Text Should Be ${DIALOG_TITLE} ${expected_title}
Element Text Should Be ${DIALOG_MESSAGE} ${expected_message}
Xác Nhận Dialog
Dialog Phải Hiển Thị
Click Element ${DIALOG_POSITIVE}
Hủy Dialog
Dialog Phải Hiển Thị
Click Element ${DIALOG_NEGATIVE}
Dialog component không biết vì sao dialog xuất hiện. Page biết hành động nào mở dialog; test biết nên xác nhận hay hủy theo scenario. Ba tầng giữ ba loại kiến thức khác nhau.
Page sử dụng component, không copy locator
Refactor resources/pages/home_page.resource:
*** Settings ***
Library AppiumLibrary
Resource resources/components/app_bar.resource
Resource resources/components/menu_item.resource
*** Keywords ***
Home Page Phải Hiển Thị
App Bar Phải Có Tiêu Đề API Demos
Menu Item "App" Phải Hiển Thị
Mở Màn Hình Search
Home Page Phải Hiển Thị
Chọn Menu Item "App"
Chọn Menu Item "Search"
Search Page Phải Hiển Thị
Mở Màn Hình Search đang dùng keyword Search Page Phải Hiển Thị, nên page cần import Search Page hoặc chuyển assertion đó sang test. Nếu Home Page import Search Page, sau này Search Page tuyệt đối không import ngược Home Page.
Phương án sạch hơn là page chỉ hoàn thành điều hướng:
Mở Màn Hình Search
Home Page Phải Hiển Thị
Chọn Menu Item "App"
Chọn Menu Item "Search"
Và test xác nhận page đích:
Mở Màn Hình Search
Search Page Phải Hiển Thị
Cách này tránh dependency vòng tròn, đồng thời test thể hiện rõ mốc điều hướng đã thành công.
Trước và sau khi có component
Trước:
Mở Menu App
Wait Until Page Contains Element accessibility_id=App
Click Element accessibility_id=App
Mở Menu Search
Wait Until Page Contains Element accessibility_id=Search
Click Element accessibility_id=Search
Sau:
Mở Màn Hình Search
Chọn Menu Item "App"
Chọn Menu Item "Search"
Phần được loại bỏ không chỉ là hai locator trùng. Chúng ta còn gom được quy tắc “menu item phải xuất hiện rồi mới click” vào một component. Khi cần đổi timeout hoặc locator strategy, mọi page dùng menu đều nhận cùng thay đổi.
Component có root locator
Không phải component nào cũng tìm được bằng một accessibility id toàn màn hình. Giả sử một list row có root theo resource-id và chứa title, subtitle, action:
*** Keywords ***
Tạo Locator Cho Result Row
[Arguments] ${title}
${root}= Set Variable
... xpath=//*[@resource-id="com.example:id/result_row"][.//*[@resource-id="com.example:id/title" and @text="${title}"]]
RETURN ${root}
Mở Result Row
[Arguments] ${title}
${root}= Tạo Locator Cho Result Row ${title}
Wait Until Page Contains Element ${root} timeout=10s
Click Element ${root}
Ví dụ này buộc phải dùng XPath để biểu diễn quan hệ row-title. Điểm quan trọng là XPath dừng ở component root và dựa trên resource-id, không bò từ root toàn màn hình qua một chuỗi index.
Với ứng dụng thật, tốt hơn là dev cung cấp ID hoặc content description trực tiếp cho row. Automation có thể vá thiếu accessibility bằng XPath, nhưng vá mãi rồi gọi đó là thiết kế thì hơi ảo ma Canada.
Test case sau khi page composition xong
*** Settings ***
Resource resources/app.resource
Resource resources/pages/home_page.resource
Resource resources/pages/search_page.resource
Test Teardown Chụp Bằng Chứng Khi Test Fail
Suite Setup Mở Ứng Dụng ApiDemos
Suite Teardown Đóng Tất Cả Appium Session
*** Test Cases ***
Người Dùng Có Thể Tìm Kiếm Nội Dung
Mở Màn Hình Search
Search Page Phải Hiển Thị
Nhập Nội Dung Tìm Kiếm Robot Framework
Gửi Yêu Cầu Tìm Kiếm
Kết Quả Tìm Kiếm Phải Là Robot Framework
Test không import component vì component là chi tiết triển khai của page. Robot Framework cho phép test gọi keyword transitively từ resource đã import, nhưng dựa vào hành vi đó để test gọi thẳng component sẽ làm ranh giới kiến trúc lại nhòe đi. Quy ước review của project phải cấm test sử dụng locator hoặc component keyword trực tiếp, trừ khi chính component là đối tượng đang được kiểm thử độc lập.
Khi nào nên tạo component?
Tạo component khi phần UI có ít nhất một trong các dấu hiệu:
- xuất hiện ở nhiều page;
- lặp thành nhiều instance có cùng cấu trúc;
- có nhiều locator con và hành vi riêng;
- thay đổi độc lập với page chứa nó.
Giữ trong Page Object khi element chỉ thuộc một màn hình và logic rất nhỏ. Dùng common keyword khi đó là utility không sở hữu UI, ví dụ tạo timestamp cho tên screenshot.
Đừng dùng số dòng làm luật kiểu “trên 50 dòng phải tách component”. Một resource 30 dòng trộn ba trách nhiệm vẫn tệ; resource 80 dòng nhưng đại diện đúng một table phức tạp vẫn có thể hợp lý. Nhìn vào quyền sở hữu, không nhìn cân nặng file.