Page Object Model in the previous lesson gathers everything belonging to a screen into the correct page. But a real application often has a toolbar appearing on ten screens, a confirmation dialog used in five flows, and a list of two dozen rows with the same structure. If we copy them into each Page Object, we just move duplicate code from the test to the resource, without completely solving the problem.

Component Object Model goes one step further: any UI part that has its own structure and behavior, which is reused or repeated, should be given its own resource.

This is a code organization convention, not a feature with a toggle in Appium, and it's not an official Robot Framework model that forces you to comply. If a project has three tiny screens but builds seventeen components, that's drawing a snake and adding legs, not clean architecture.

Page Object diagram combining App Bar, Menu Item, and Dialog Component
Page Object diagram combining App Bar, Menu Item, and Dialog Component

The diagram I created to indicate boundaries of responsibility: pages create compositions, components own locators and behaviors of reusable UI parts.

How is Page different from Component?

ObjectRepresentsOwnsExample
Page ObjectA screen or main navigation stateScreen-level locator, behavior and orchestrationHome Page, Search Page
Component ObjectA UI area with its own structure/behaviorRoot, child locators, and component actionsToolbar, menu row, dialog
Common keywordA technical action not belonging to a specific UISmall utility, does not hold screen locatorsTake screenshot name, normalize text

Components do not replace pages. Pages use components to complete the screen's behavior.

Test caseHome PageApp Bar componentMenu Item componentDialog componentAppiumLibrary
// Mermaid diagram

Extended project structure

resources/
├── app.resource
├── components/
│   ├── app_bar.resource
│   ├── menu_item.resource
│   └── dialog.resource
└── pages/
    ├── home_page.resource
    └── search_page.resource

components is not a place to throw in every unknown keyword. A component must be able to answer three questions:

  1. What UI area does it represent?
  2. Where is its root or boundary?
  3. Which page is using its behavior?

If you cannot answer, it is very likely that you are creating the renamed version of common.resource.

In ApiDemos, menu items can be found by accessibility id. Create resources/components/menu_item.resource:

*** Settings ***
Library    AppiumLibrary

*** Keywords ***
Menu Item "${label}" Should Be Visible
    ${locator}=    Build Menu Item Locator    ${label}
    Wait Until Page Contains Element    ${locator}    timeout=10s

Select Menu Item "${label}"
    ${locator}=    Build Menu Item Locator    ${label}
    Wait Until Page Contains Element    ${locator}    timeout=10s
    Click Element    ${locator}

Build Menu Item Locator
    [Arguments]    ${label}
    ${locator}=    Set Variable    accessibility_id=${label}
    RETURN    ${locator}

Select Menu Item "App" is an embedded argument: Robot Framework takes the part between the two quotes as ${label}. Test or page calls very naturally:

Select Menu Item "App"
Select Menu Item "Search"

The keyword for creating a locator is grouped separately so that displaying and clicking do not automatically assemble the string in two different ways. If later the menu switches to resource-id, only one place needs to be fixed.

Because ${label} goes straight to the accessibility id, there is no XPath escape problem. If you build a dynamic XPath from any input, you have to handle the quote marks and not allow test data to arbitrarily turn into a selector expression.

App bar: a component with a fixed locator

Create 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 Title Should Be
    [Arguments]    ${expected_title}
    Wait Until Page Contains Element    ${APP_BAR_ROOT}    timeout=10s
    Element Text Should Be    ${APP_BAR_TITLE}    ${expected_title}

Navigate Back With App Bar
    Wait Until Element Is Visible    ${APP_BAR_NAVIGATE_UP}    timeout=10s
    Click Element    ${APP_BAR_NAVIGATE_UP}

Root ${APP_BAR_ROOT} helps confirm that the component has appeared before reading the title. If the application uses a custom toolbar without android:id/action_bar_*, the component still retains the API keyword, only the internal locator is changed.

Dialog: component with title, message, and action

Create 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 Should Be Visible
    Wait Until Element Is Visible    ${DIALOG_ROOT}    timeout=10s

Dialog Content Should Be
    [Arguments]    ${expected_title}    ${expected_message}
    Dialog Should Be Visible
    Element Text Should Be    ${DIALOG_TITLE}    ${expected_title}
    Element Text Should Be    ${DIALOG_MESSAGE}    ${expected_message}

Confirm Dialog
    Dialog Should Be Visible
    Click Element    ${DIALOG_POSITIVE}

Cancel Dialog
    Dialog Should Be Visible
    Click Element    ${DIALOG_NEGATIVE}

The dialog component does not know why the dialog appears. The page knows which action opens the dialog; the test knows whether to confirm or cancel according to the scenario. The three layers hold three different types of knowledge.

The page uses components instead of copying locators

Refactor resources/pages/home_page.resource:

*** Settings ***
Library     AppiumLibrary
Resource    resources/components/app_bar.resource
Resource    resources/components/menu_item.resource

*** Keywords ***
Home Page Should Be Visible
    App Bar Title Should Be    API Demos
    Menu Item "App" Should Be Visible

Open The Search Screen
    Home Page Should Be Visible
    Select Menu Item "App"
    Select Menu Item "Search"
    Search Page Should Be Visible

Open The Search Screen is using the keyword Search Page Should Be Visible, so the page needs to import Search Page or move that assertion to the test. If Home Page imports Search Page, later on Search Page should absolutely not import Home Page back.

A cleaner approach is for the page to only complete the navigation:

Open The Search Screen
    Home Page Should Be Visible
    Select Menu Item "App"
    Select Menu Item "Search"

And test to confirm the landing page:

Open The Search Screen
Search Page Should Be Visible

This method avoids circular dependency, while the test clearly shows that the navigation milestone has been successful.

Before and after having a component

Before:

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:

Open The Search Screen
    Select Menu Item "App"
    Select Menu Item "Search"

The part that was removed was not just the two duplicate locators. We also consolidated the rule "menu item must appear before clicking" into a single component. When we need to change the timeout or locator strategy, all pages using the menu will receive the same change.

Component with root locator

Not every component can be found using a fullscreen accessibility id. Suppose a list row has a root by resource-id and contains 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}

This example must use XPath to represent the row-title relationship. The important point is that XPath stops at the component root and relies on the resource-id, not crawling from the full screen root through a series of indexes.

For a real application, it's better for the dev to provide an ID or content description directly for the row. Automation can patch missing accessibility with XPath, but if you keep patching it and call that design, it's a bit of a Canadian illusion.

Test case after page composition is done

*** Settings ***
Resource          resources/app.resource
Resource          resources/pages/home_page.resource
Resource          resources/pages/search_page.resource
Test Teardown     Capture Evidence When Test Fails
Suite Setup       Open The ApiDemos Application
Suite Teardown    Close All Appium Sessions

*** Test Cases ***
Người Dùng Có Thể Tìm Kiếm Nội Dung
    Open The Search Screen
    Search Page Should Be Visible
    Enter Search Query    Robot Framework
    Submit Search
    Search Result Should Be    Robot Framework

Tests do not import components because components are an implementation detail of a page. Robot Framework allows tests to call keywords transitively from imported resources, but relying on that behavior to test by calling components directly will blur the architectural boundaries. The project's review convention must prohibit tests from using locators or component keywords directly, unless the component itself is the object being tested independently.

When should you create a component?

Create a component when the UI has at least one of the following signs:

Keep it in the Page Object when the element belongs to only one screen and the logic is very small. Use a common keyword when it is a utility that does not own a UI, for example creating a timestamp for a screenshot name.

Don't use line count as a rule like 'over 50 lines, you must split the component.' A 30-line resource mixing three responsibilities is still bad; an 80-line resource that represents a single complex table can still be reasonable. Look at ownership, not file weight.

References