{"id":21523215,"url":"https://github.com/testcentricity/testcentricity_apps","last_synced_at":"2026-02-04T23:41:10.272Z","repository":{"id":246240321,"uuid":"820201181","full_name":"TestCentricity/testcentricity_apps","owner":"TestCentricity","description":"TestCentricity™ For Apps core POM test automation framework gem","archived":false,"fork":false,"pushed_at":"2024-11-13T19:07:01.000Z","size":2812,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-24T00:38:54.062Z","etag":null,"topics":["android","appium","ios","ipados","macos-app-automation","mobile-app-automation","ruby-gem","screen-object-model","test-automation"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/TestCentricity.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-06-26T02:26:15.000Z","updated_at":"2024-11-13T19:06:16.000Z","dependencies_parsed_at":"2024-06-26T19:41:34.846Z","dependency_job_id":null,"html_url":"https://github.com/TestCentricity/testcentricity_apps","commit_stats":null,"previous_names":["testcentricity/testcentricity_apps"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TestCentricity%2Ftestcentricity_apps","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TestCentricity%2Ftestcentricity_apps/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TestCentricity%2Ftestcentricity_apps/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TestCentricity%2Ftestcentricity_apps/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/TestCentricity","download_url":"https://codeload.github.com/TestCentricity/testcentricity_apps/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248124869,"owners_count":21051757,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["android","appium","ios","ipados","macos-app-automation","mobile-app-automation","ruby-gem","screen-object-model","test-automation"],"created_at":"2024-11-24T01:12:59.939Z","updated_at":"2026-02-04T23:41:10.262Z","avatar_url":"https://github.com/TestCentricity.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# TestCentricity™ For Apps\n\n[![Gem Version](https://badge.fury.io/rb/testcentricity_apps.svg)](https://badge.fury.io/rb/testcentricity_apps)\n[![License (3-Clause BSD)](https://img.shields.io/badge/license-BSD%203--Clause-blue.svg?style=flat-square)](http://opensource.org/licenses/BSD-3-Clause)\n![Gem Downloads](https://img.shields.io/gem/dt/testcentricity_apps)\n![Maintained](https://img.shields.io/badge/maintenance-actively--developed-brightgreen.svg)\n[![Docs](https://img.shields.io/badge/docs-rubydoc-blue.svg)](http://www.rubydoc.info/gems/testcentricity_apps)\n\n\nThe TestCentricity™ For Apps core framework for MacOS desktop apps and mobile iOS/iPadOS and Android app testing implements\na Screen Object Model DSL for use with Cucumber or RSpec and Appium version 2.x. It also facilitates the configuration of\nthe appropriate Appium capabilities and driver required to establish a connection with locally hosted MacOS desktop apps,\nor locally or cloud hosted iOS and Android real devices or simulators.\n\nThe TestCentricity™ For Apps gem supports automated testing of MacOS desktop apps and native iOS and Android apps running\non the following mobile test targets:\n* locally hosted MacOS desktop apps (using Appium 2.x, the Mac2 driver, and XCode on macOS)\n* locally hosted iOS device simulators or physical iOS devices (using Appium 2.x, the XCUItest driver, and XCode on macOS)\n* locally hosted Android devices or Android Studio virtual device emulators (using Appium 2.x, the UIAutomator2 driver, and Android Studio)\n* cloud hosted iOS or Android physical devices and simulators from the following service:\n  * [Browserstack](https://www.browserstack.com/list-of-browsers-and-platforms/app_automate)\n  * [Sauce Labs](https://saucelabs.com/platform/mobile-testing)\n  * [TestingBot](https://testingbot.com/mobile/realdevicetesting)\n\n\n## What's New\n\nA complete history of bug fixes and new features can be found in the [CHANGELOG](https://github.com/TestCentricity/testcentricity_apps/blob/master/CHANGELOG.md) file.\n\nThe RubyDocs for this gem can be found [here](https://www.rubydoc.info/gems/testcentricity_apps/).\n\nThree example projects that demonstrates the implementation of a screen object model framework using TestCentricity™ For Apps\nand Cucumber can be found at the following:\n* [tc_mac_calculator_demo](https://github.com/TestCentricity/tc_mac_calculator_demo)\n* [tc_mobile_react_native_demo](https://github.com/TestCentricity/tc_mobile_react_native_demo)\n* [tc_mobile_wdio_demo](https://github.com/TestCentricity/tc_mobile_wdio_demo)\n\nRefer to [this wiki page](https://github.com/TestCentricity/testcentricity_apps/wiki/XCUItest-driver-bug-impacts-iOS-dialogs-managed-by-com.apple.springboard) for information on a bug with the latest versions of the XCUItest driver that affects Appium's\nability to interact with and verify iOS system level modal dialogs.\n\n\n### Which gem should I use?\n\n* The [TestCentricity For **Apps** gem](https://rubygems.org/gems/testcentricity_apps) supports testing of MacOS desktop apps and native iOS and Android mobile apps\n* The [TestCentricity For **Mobile** gem](https://rubygems.org/gems/testcentricity_mobile) supports testing of native iOS and Android mobile apps\n* The [TestCentricity For **Web** gem](https://rubygems.org/gems/testcentricity_web) supports testing of web interfaces via desktop and mobile web browsers\n\n| Tested platforms                                  | TestCentricity For Apps | TestCentricity For Mobile | TestCentricity For Web |\n|---------------------------------------------------|:-:|:-:|:-:|\n| MacOS desktop apps                                | Yes                     | No                        | No                     |\n| Native mobile iOS/iPadOS and/or Android apps only | Yes                     | Yes                       | No                     |\n| Desktop/mobile web browsers only                  | No                      | No                        | Yes                    |\n\n\n## Installation\n\nTestCentricity For Apps requires Ruby 3.1.0 or later. To install the TestCentricity For Apps gem, add this line to your\nautomation project's `Gemfile`:\n\n    gem 'testcentricity_apps'\n\nAnd then execute:\n\n    $ bundle\n\nOr install it yourself as:\n\n    $ gem install testcentricity_apps\n\n\n---\n## Setup\n### Using Cucumber\n\nIf you are using Cucumber, you need to require the following in your `env.rb` file:\n```ruby\n    require 'testcentricity_apps'\n```\n\n### Using RSpec\n\nIf you are using RSpec instead, you need to require the following in your `spec_helper.rb` file:\n```ruby\n    require 'testcentricity_apps'\n```\n\n---\n## ScreenObjects\n\nThe **Screen Object Model** is a test automation pattern that aims to create an abstraction of your MacOS desktop app or\nnative mobile app's User Interface that can be used in tests. The **Screen** Object Model in MacOS desktop apps or native\nmobile app test automation is equivalent to the **Page** Object Model in web user interface test automation.\n\nA **Screen Object** is an object that represents a single screen in your AUT (Application Under Test). **Screen Objects**\nencapsulate the implementation details of a MacOS desktop or native mobile app screen and expose an API that supports\ninteraction with, and validation of the UI elements on the screen.\n\n**Screen Objects** makes it easier to maintain automated tests because changes to screen UI elements are updated in only\none location - in the `ScreenObject` class definition. By adopting a **Screen Object Model**, Cucumber feature files and\nstep definitions are no longer required to hold specific information about a screen's UI objects, thus minimizing maintenance\nrequirements. If any element on, or property of a screen changes (text field attributes, button captions, element states,\netc.), maintenance is performed in the `ScreenObject` class definition only, typically with no need to update the affected\nfeature files, scenarios, or step definitions.\n\n\n### Defining a ScreenObject\n\nYour `ScreenObject` class definitions should be contained within individual `.rb` files in the `features/support/\u003cplatform\u003e/screens`\nfolder of your test automation project, where `\u003cplatform\u003e` is typically `mac`, `ios`, or `android`. For each screen in your app,\nyou will typically have to define a `ScreenObject` for each platform version of your app.\n\n        📁 my_automation_project\n        ├── 📁 config\n        ├── 📁 features\n        │   ├── 📁 step_definitions\n        │   ├── 📁 support\n        │   │   ├── 📁 android\n        |   |   |   └── 📁 screens\n        │   │   ├── 📁 ios\n        |   |   |   └── 📁 screens\n        │   │   ├── 📁 mac\n        |   |   |   └── 📁 screens\n        │   │   ├── 📄 env.rb\n        │   │   └── 📄 hooks.rb\n        ├── 📄 Gemfile\n        └── 📄 README.md\n\n\nYou define a new `ScreenObject` as shown below:\n```ruby\n    class LoginScreen \u003c TestCentricity::ScreenObject\n    end\n    \n    \n    class ProductsScreen \u003c TestCentricity::ScreenObject\n    end\n    \n    \n    class CheckoutAddressScreen \u003c TestCentricity::ScreenObject\n    end\n```\n\n### Adding Traits to your ScreenObject\n\nDesktop and mobile app screens typically have names associated with them. Screens also typically have a unique object or\nattribute that, when present, indicates that the screen's contents have fully loaded.\n\nThe `screen_name` trait is registered with the `ScreenManager` object, which includes a `find_screen` method that takes a\nscreen name as a parameter and returns an instance of the associated `ScreenObject`. If you intend to use the `ScreenManager`,\nyou must define a`screen_name` trait for each `ScreenObject` to be registered.\n\nThe `screen_name` trait is usually a `String` value that represents the name of the screen that will be matched by the\n`ScreenManager.find_screen` method. `screen_name` traits are case and white-space sensitive. For screens that may be\nreferenced with multiple names, the `screen_name` trait may also be an `Array` of `String` values representing those\nscreen names.\n\nThe `screen_locator` trait specifies a locator for a unique object that exists once the screen's contents have been fully\nrendered. The `screen_locator` trait is a locator strategy that uniquely identifies the object. The `ScreenObject.verify_screen_exists`\nmethod waits for the `screen_locator` trait to exist, and raises an exception if the wait time exceeds the `default_max_wait_time`.\n\nA `deep_link` trait should be defined if a screen can be directly loaded using a deep link. Specifying a `deep_link` trait\nis optional, as not all screens can be directly accessed via a deep link.\n\nYou define your screen's **Traits** as shown below:\n```ruby\n    class LoginScreen \u003c TestCentricity::ScreenObject\n      trait(:screen_name)    { 'Login' }\n      trait(:screen_locator) { { accessibility_id: 'login screen' } }\n      trait(:deep_link)      { 'mydemoapprn://login' }\n    end\n    \n    \n    class ProductsScreen \u003c TestCentricity::ScreenObject\n      trait(:screen_name)    { 'Products' }\n      trait(:screen_locator) { { accessibility_id: 'products screen' } }\n      trait(:deep_link)      { 'mydemoapprn://store-overview' }\n    end\n    \n    \n    class CheckoutAddressScreen \u003c TestCentricity::ScreenObject\n      trait(:screen_name)    { 'Checkout - Address' }\n      trait(:screen_locator) { { accessibility_id: 'checkout address screen' } }\n      trait(:deep_link)      { 'mydemoapprn://checkout-address' }\n    end\n```\n\n### Adding UI Elements to your ScreenObject\n\nDesktop and mobile app screens are made up of UI elements like text fields, check boxes, radio buttons, switches, lists,\nbuttons, etc. **UI Elements** are added to your `ScreenObject` class definition as shown below:\n```ruby\n    class LoginScreen \u003c TestCentricity::ScreenObject\n      trait(:screen_name)    { 'Login' }\n      trait(:screen_locator) { { accessibility_id: 'login screen' } }\n      trait(:deep_link)      { 'mydemoapprn://login' }\n    \n      # Login screen UI elements\n      labels     username_label: { accessibility_id: 'Username'},\n                 password_label: { xpath: '(//XCUIElementTypeStaticText[@name=\"Password\"])[1]'},\n                 username_error: { accessibility_id: 'Username-error-message' },\n                 password_error: { accessibility_id: 'Password-error-message' },\n                 generic_error:  { accessibility_id: 'generic-error-message' }\n      textfields username_field: { accessibility_id: 'Username input field' },\n                 password_field: { accessibility_id: 'Password input field' }\n      button     :login_button,  { accessibility_id: 'Login button' }\n    end\n\n\n    class CheckoutAddressScreen \u003c TestCentricity::ScreenObject\n      trait(:screen_name)    { 'Checkout - Address' }\n      trait(:screen_locator) { { accessibility_id: 'checkout address screen' } }\n      trait(:deep_link)      { 'mydemoapprn://checkout-address' }\n    \n      # Checkout Address screen UI elements\n      textfields fullname_field:     { accessibility_id: 'Full Name* input field' },\n                 address1_field:     { accessibility_id: 'Address Line 1* input field' },\n                 address2_field:     { accessibility_id: 'Address Line 2 input field' },\n                 city_field:         { accessibility_id: 'City* input field' },\n                 state_region_field: { accessibility_id: 'State/Region input field' },\n                 zip_code_field:     { accessibility_id: 'Zip Code* input field' },\n                 country_field:      { accessibility_id: 'Country* input field' }\n      button     :to_payment_button, { accessibility_id: 'To Payment button' }\n    end\n```\n\n### Adding Methods to your ScreenObject\n\nIt is good practice for your Cucumber step definitions to call high level methods in your your `ScreenObject` instead of\ndirectly accessing and interacting with a screen object's UI elements. You can add high level methods to your `ScreenObject`\nclass definition for interacting with the UI to hide implementation details, as shown below:\n```ruby\n    class LoginScreen \u003c TestCentricity::ScreenObject\n      trait(:screen_name)    { 'Login' }\n      trait(:screen_locator) { { accessibility_id: 'login screen' } }\n      trait(:deep_link)      { 'mydemoapprn://login' }\n    \n      # Login screen UI elements\n      labels     username_label: { accessibility_id: 'Username'},\n                 password_label: { xpath: '(//XCUIElementTypeStaticText[@name=\"Password\"])[1]'},\n                 username_error: { accessibility_id: 'Username-error-message' },\n                 password_error: { accessibility_id: 'Password-error-message' },\n                 generic_error:  { accessibility_id: 'generic-error-message' }\n      textfields username_field: { accessibility_id: 'Username input field' },\n                 password_field: { accessibility_id: 'Password input field' }\n      button     :login_button,  { accessibility_id: 'Login button' }\n    \n      def verify_screen_ui\n        ui = {\n          header_label   =\u003e { visible: true, caption: 'Login' },\n          username_label =\u003e { visible: true, caption: 'Username' },\n          username_field =\u003e { visible: true, enabled: true },\n          password_label =\u003e { visible: true, caption: 'Password' },\n          password_field =\u003e { visible: true, enabled: true },\n          login_button   =\u003e { visible: true, enabled: true, caption: 'Login' }\n        }\n        verify_ui_states(ui)\n      end\n    \n      def login(username, password)\n        fields = {\n          username_field =\u003e username,\n          password_field =\u003e password\n        }\n        populate_data_fields(fields)\n        login_button.tap\n      end\n    \n      def verify_entry_error(reason)\n        ui = case reason.gsub(/\\s+/, '_').downcase.to_sym\n             when :invalid_password, :invalid_user\n               { generic_error =\u003e { visible: true, caption: 'Provided credentials do not match any user in this service.' } }\n             when :locked_account\n               { generic_error =\u003e { visible: true, caption: 'Sorry, this user has been locked out.' } }\n             when :no_username\n               { username_error =\u003e { visible: true, caption: 'Username is required' } }\n             when :no_password\n               { password_error =\u003e { visible: true, caption: 'Password is required' } }\n             else\n               raise \"#{reason} is not a valid selector\"\n             end\n        verify_ui_states(ui)\n      end\n    end\n```\n\nOnce your `ScreenObject` has been instantiated, you can call your methods as shown below:\n```ruby\n    login_screen.login('snicklefritz', 'Pa55w0rd')\n    login_screen.verify_entry_error('invalid user')\n```\n\n### Loading your App's ScreenObjects using Deeplinks\n\nUsers typically move between an app's screens (or a web portal's pages) by interacting with various navigation metaphors,\nusually by tapping on buttons or links, or making selections from menu, grid, carousel, or list items. When testing web\ninterfaces using automated tests, time consuming interactions with the user interface can usually be reduced by using URLs\nto quickly load pages without following a strict workflow.\n\nBeing able to use a combination of public or private APIs and URLs to bypass the time consuming interactions with a user\ninterface that may be undergoing refactoring during ongoing development (and which could lead to test failures due to bugs\nin the new UI) can result in significant reduction in test execution time. While all UI interactions should be comprehensively\ntested, most of the repetitive time intensive UI workflow interactions required to establish a stable base state for testing\ndownstream functionality can be avoided by leveraging testability \"shortcuts\" provided by your app's developers.\n\nFor example, in order to verify the functionality of finalizing the purchase of products via an ecommerce app or web portal,\na typical workflow might require a user to search for products to purchase, select product specific options (color, size,\nquantity, etc.), add the products to a shopping cart, and log in to their account before they can finalize the purchase.\nBy utilizing developer provided APIs, URLs, or deeplinks, test execution time can be greatly reduced.\n\nThe `ScreenObject.load_screen` method is used to load a screen using its defined `deep_link` trait. When testing on physical\niOS devices running iOS/iPadOS versions earlier than version 16.4, deep links can only be opened by sending the deeplink URL\nto the mobile Safari web browser, and then accepting the confirmation modal that pops up. The `load_screen` method handles\ninvoking deeplinks on Android and iOS/iPadOS simulators and physical devices.\n\nRefer to the [Speeding Up Tests With Deep Links](https://appiumpro.com/editions/7-speeding-up-tests-with-deep-links) post on [AppiumPro](https://appiumpro.com/) for more information about deeplinks.\n\n\n---\n## ScreenSections\n\nA `ScreenSection` is a collection of **UI Elements** that may appear in multiple locations on a screen, or on multiple\nscreens in an app. It is a collection of **UI Elements** that represent a conceptual area of functionality, like a menu,\na navigation bar, or a search capability. **UI Elements** and functional behavior are confined to the scope of a `ScreenSection`\nobject. A `ScreenSection` may contain other `ScreenSection` objects.\n\nBelow is an example of a footer navigation bar feature that is common to multiple screen -\n\n![Navigation Footer](https://raw.githubusercontent.com/TestCentricity/testcentricity_apps/main/.github/images/NavBar1.png \"Navigation Footer\")       ![Navigation Footer](https://raw.githubusercontent.com/TestCentricity/testcentricity_apps/main/.github/images/NavBar2.png \"Navigation Footer\")\n\n\n### Defining a ScreenSection\n\nYour `ScreenSection` class definitions should be contained within individual `.rb` files in the `features/support/\u003cplatform\u003e/sections`\nfolder of your test automation project, where `\u003cplatform\u003e` is typically `mac`, `ios`, or `android`. For each screen section in your\napp, you will typically have to define a `ScreenSection` for each platform version of your app.\n\n        📁 my_automation_project\n        ├── 📁 config\n        ├── 📁 features\n        │   ├── 📁 step_definitions\n        │   ├── 📁 support\n        │   │   ├── 📁 android\n        |   |   |   ├── 📁 screens\n        |   |   |   └── 📁 sections\n        │   │   ├── 📁 ios\n        |   |   |   ├── 📁 screens\n        |   |   |   └── 📁 sections\n        │   │   ├── mac\n        |   |   |   ├── 📁 screens\n        |   |   |   └── 📁 sections\n        │   │   ├── 📄 env.rb\n        │   │   └── 📄 hooks.rb\n        ├── 📄 Gemfile\n        └── 📄 README.md\n\n\nYou define a new `ScreenSection` as shown below:\n```ruby\n    class NavMenu \u003c TestCentricity::ScreenSection\n    end\n```\n\n### Adding Traits to a ScreenSection\n\nA `ScreenSection` typically has a root node object that encapsulates a collection of `UIElements`. The `section_locator`\ntrait specifies the CSS or Xpath expression that uniquely identifies that root node object.\n\nYou define your section's **Traits** as shown below:\n```ruby\n    class NavMenu \u003c TestCentricity::ScreenSection\n      trait(:section_name)    { 'Nav Menu' }\n      trait(:section_locator) { { xpath: '//XCUIElementTypeScrollView' } }\n    end\n```\n\n### Adding UI Elements to your ScreenSection\n\nA `ScreenSection` is typically made up of UI elements like text fields, check boxes, switches, lists, buttons, etc. **UI\nElements** are added to your `ScreenSection` class definition as shown below:\n```ruby\n    class NavMenu \u003c TestCentricity::ScreenSection\n      trait(:section_name)    { 'Nav Menu' }\n      trait(:section_locator) { { xpath: '//XCUIElementTypeScrollView' } }\n    \n      # Nav Menu UI elements\n      buttons close_button:        { accessibility_id: 'close menu' },\n              webview_button:      { accessibility_id: 'menu item webview' },\n              qr_code_button:      { accessibility_id: 'menu item qr code scanner' },\n              geo_location_button: { accessibility_id: 'menu item geo location' },\n              drawing_button:      { accessibility_id: 'menu item drawing' },\n              report_a_bug_button: { accessibility_id: 'menu item report a bug' },\n              about_button:        { accessibility_id: 'menu item about' },\n              reset_app_button:    { accessibility_id: 'menu item reset app' },\n              biometrics_button:   { accessibility_id: 'menu item biometrics' },\n              log_in_button:       { accessibility_id: 'menu item log in' },\n              log_out_button:      { accessibility_id: 'menu item log out' },\n              api_calls_button:    { accessibility_id: 'menu item api calls' },\n              sauce_video_button:  { accessibility_id: 'menu item sauce bot video' }\n    end\n```\n\n### Adding Methods to your ScreenSection\n\nYou can add methods to your `ScreenSection` class definition, as shown below:\n```ruby\n    class NavMenu \u003c TestCentricity::ScreenSection\n      trait(:section_name)    { 'Nav Menu' }\n      trait(:section_locator) { { xpath: '//XCUIElementTypeScrollView' } }\n    \n      # Nav Menu UI elements\n      buttons close_button:        { accessibility_id: 'close menu' },\n              webview_button:      { accessibility_id: 'menu item webview' },\n              qr_code_button:      { accessibility_id: 'menu item qr code scanner' },\n              geo_location_button: { accessibility_id: 'menu item geo location' },\n              drawing_button:      { accessibility_id: 'menu item drawing' },\n              report_a_bug_button: { accessibility_id: 'menu item report a bug' },\n              about_button:        { accessibility_id: 'menu item about' },\n              reset_app_button:    { accessibility_id: 'menu item reset app' },\n              biometrics_button:   { accessibility_id: 'menu item biometrics' },\n              log_in_button:       { accessibility_id: 'menu item log in' },\n              log_out_button:      { accessibility_id: 'menu item log out' },\n              api_calls_button:    { accessibility_id: 'menu item api calls' },\n              sauce_video_button:  { accessibility_id: 'menu item sauce bot video' }\n    \n      def verify_ui\n        ui = {\n                self                =\u003e { visible: true },\n                close_button        =\u003e { visible: true, enabled: true },\n                webview_button      =\u003e { visible: true, enabled: true, caption: 'Webview' },\n                qr_code_button      =\u003e { visible: true, enabled: true, caption: 'QR Code Scanner' },\n                geo_location_button =\u003e { visible: true, enabled: true, caption: 'Geo Location' },\n                drawing_button      =\u003e { visible: true, enabled: true, caption: 'Drawing' },\n                report_a_bug_button =\u003e { visible: true, enabled: true, caption: 'Report A Bug' },\n                about_button        =\u003e { visible: true, enabled: true, caption: 'About' },\n                reset_app_button    =\u003e { visible: true, enabled: true, caption: 'Reset App State' },\n                biometrics_button   =\u003e { visible: true, enabled: true, caption: 'FaceID' },\n                log_in_button       =\u003e { visible: true, enabled: true, caption: 'Log In' },\n                log_out_button      =\u003e { visible: true, enabled: true, caption: 'Log Out' },\n                api_calls_button    =\u003e { visible: true, enabled: true, caption: 'Api Calls' },\n                sauce_video_button  =\u003e { visible: true, enabled: true, caption: 'Sauce Bot Video' }\n        }\n        verify_ui_states(ui)\n      end\n    \n      def close\n        close_button.click\n        self.wait_until_hidden(3)\n      end\n    \n      def verify_closed\n        ui = {\n                self =\u003e { visible: true },\n                close_button =\u003e { visible: false }\n        }\n        verify_ui_states(ui)\n      end\n    end\n```\n\n### Adding ScreenSections to your ScreenObject\n\nYou add a `ScreenSection` to its associated `ScreenObject` as shown below:\n```ruby\n    class BaseAppScreen \u003c TestCentricity::ScreenObject\n      # Base App screen UI elements\n      label    :header_label, { accessibility_id: 'container header' }\n      sections nav_bar:  NavBar,\n               nav_menu: NavMenu\n    end\n```\nOnce your `ScreenObject` has been instantiated, you can call its `ScreenSection` methods as shown below:\n```ruby\n    base_screen.nav_menu.verify_ui\n```\n\n---\n## AppUIElements\n\nNative app `ScreenObjects` and `ScreenSections` are typically made up of **UI Element** like text fields, switches, lists,\nbuttons, etc. **UI Elements** are declared and instantiated within the class definition of the `ScreenObject` or `ScreenSection`\nin which they are contained. With TestCentricity, all native app screen UI elements are based on the `AppUIElement` class.\n\n\n### Declaring and Instantiating AppUIElements\n\nSingle `AppUIElement` declarations have the following format:\n\n    elementType :elementName, { locator_strategy: locator_identifier }\n\n* The `elementName` is the unique name that you will use to refer to the UI element and is specified as a `Symbol`.\n* The `locator_strategy` specifies the selector strategy that Appium will use to find the `AppUIElement`. Valid selectors are:\n  - `accessibility_id:`\n  - `id:`\n  - `name:`\n  - `class:`\n  - `xpath:`\n  - `predicate:` (MacOS and iOS only)\n  - `class_chain:` (MacOS and iOS only)\n  - `uiautomator:` (Android only)\n  - `css:` (WebViews in hybrid mobile apps only).\n* The `locator_identifier` is the value or attribute that uniquely and unambiguously identifies the `AppUIElement`.\n\nRefer to [this page](https://github.com/appium/appium-mac2-driver?tab=readme-ov-file#element-location) for information on selector strategies for MacOS.\nRefer to [this page](https://appium.github.io/appium-xcuitest-driver/5.12/locator-strategies/) for information on selector strategies for iOS.\nRefer to [this page](https://github.com/appium/appium-uiautomator2-driver?tab=readme-ov-file#element-location) for information on selector strategies for Android.\n\nMultiple `AppUIElement` declarations for a collection of elements of the same type can be performed by passing a hash table\ncontaining the names and locators of each individual element.\n\n### Example AppUIElement Declarations\n\nSupported `AppUIElement` elementTypes and their declarations have the following format:\n\n*Single element declarations:*\n```ruby\n    class SampleScreen \u003c TestCentricity::ScreenObject\n      button     :button_name, { locator_strategy: locator_identifier }\n      textfield  :field_name, { locator_strategy: locator_identifier }\n      checkbox   :checkbox_name, { locator_strategy: locator_identifier }\n      radio      :radio_name, { locator_strategy: locator_identifier }\n      label      :label_name, { locator_strategy: locator_identifier }\n      list       :list_name, { locator_strategy: locator_identifier }\n      selectlist :selectlist_name, { locator_strategy: locator_identifier }\n      image      :image_name, { locator_strategy: locator_identifier }\n      switch     :switch_name, { locator_strategy: locator_identifier }\n      element    :element_name, { locator_strategy: locator_identifier }\n      alert      :alert_name, { locator_strategy: locator_identifier }\n      menu       :menu_name, { locator_strategy: locator_identifier }\n      menubar    :menubar_name, { locator_strategy: locator_identifier }\n      table      :table_name, { locator_strategy: locator_identifier }\n    end\n```\n*Multiple element declarations:*\n```ruby\n    class SampleScreen \u003c TestCentricity::ScreenObject\n      buttons     button_1_name: { locator_strategy: locator_identifier },\n                  button_2_name: { locator_strategy: locator_identifier },\n                  button_X_name: { locator_strategy: locator_identifier }\n      textfields  field_1_name: { locator_strategy: locator_identifier },\n                  field_2_name: { locator_strategy: locator_identifier },\n                  field_X_name: { locator_strategy: locator_identifier }\n      checkboxes  check_1_name: { locator_strategy: locator_identifier },\n                  check_2_name: { locator_strategy: locator_identifier },\n                  check_X_name: { locator_strategy: locator_identifier }\n      radios      radio_1_name: { locator_strategy: locator_identifier },\n                  radio_2_name: { locator_strategy: locator_identifier }\n      lists       list_1_name: { locator_strategy: locator_identifier },\n                  list_X_name: { locator_strategy: locator_identifier }\n      selectlists menu_1_name: { locator_strategy: locator_identifier },\n                  menu_X_name: { locator_strategy: locator_identifier }\n      labels      label_1_name: { locator_strategy: locator_identifier },\n                  label_X_name: { locator_strategy: locator_identifier }\n      images      image_1_name: { locator_strategy: locator_identifier },\n                  image_X_name: { locator_strategy: locator_identifier }\n      alerts      alert_1_name: { locator_strategy: locator_identifier },\n                  alert_X_name: { locator_strategy: locator_identifier }\n      tables      table_1_name: { locator_strategy: locator_identifier },\n                  table_X_name: { locator_strategy: locator_identifier }\n    end\n```\nRefer to the Class List documentation for the `ScreenObject` and `ScreenSection` classes for details on the class methods\nused for declaring and instantiating `AppUIElements`. Examples of UI element declarations can be found in the ***Adding\nUI Elements to your ScreenObject*** and ***Adding UI Elements to your ScreenSection*** sections above.\n\n\n### AppUIElement Inherited Methods\n\nWith TestCentricity, all native app UI elements are based on the `AppUIElement` class, and inherit the following methods:\n\n**Action methods:**\n\n    element.click\n    element.tap\n    element.double_tap\n    element.long_press\n    element.scroll_into_view\n    element.drag_by(right_offset, down_offset)\n    element.drag_and_drop(target)\n    element.swipe_gesture(direction, distance)\n\n**Object state methods:**\n\n    element.exists?\n    element.visible?\n    element.hidden?\n    element.enabled?\n    element.disabled?\n    element.selected?\n    element.tag_name\n    element.width\n    element.height\n    element.x_loc\n    element.y_loc\n    element.count\n    element.get_attribute(attrib)\n    element.identifier  (MacOS only)\n\n**Waiting methods:**\n\n    element.wait_until_exists(seconds)\n    element.wait_until_gone(seconds)\n    element.wait_until_visible(seconds)\n    element.wait_until_hidden(seconds)\n    element.wait_until_enabled(seconds)\n    element.wait_until_value_is(value, seconds)\n    element.wait_until_value_changes(seconds)\n\n\n### Populating your ScreenObject or ScreenSection with data\n\nA typical automated test may be required to perform the entry of test data by interacting with various `AppUIElements` on\nyour `ScreenObject` or `ScreenSection`. This data entry can be performed using the various object action methods (listed\nabove) for each `AppUIElement` that needs to be interacted with.\n\nThe `ScreenObject.populate_data_fields` and `ScreenSection.populate_data_fields` methods support the entry of test data\ninto a collection of `AppUIElements`. The `populate_data_fields` method accepts a hash containing key/hash pairs of\n`AppUIElements` and their associated data to be entered. Data values must be in the form of a `String` for `textfield`\ncontrols. For `checkbox`, `radio`, and `switch` controls, data must either be a `Boolean` or a `String` that evaluates to\na `Boolean` value ('Yes', 'No', '1', '0', 'true', 'false').\n\nThe `populate_data_fields` method verifies that data attributes associated with each `AppUIElement` is not `nil` or `empty`\nbefore attempting to enter data into the `AppUIElement`.\n\nThe optional `wait_time` parameter is used to specify the time (in seconds) to wait for each `AppUIElement` to become\nviable for data entry (the `AppUIElement` must be visible and enabled) before entering the associated data value. This\noption is useful in situations where entering data, or setting the state of a `AppUIElement` might cause other `AppUIElements`\nto become visible or active. Specifying a wait_time value ensures that the subsequent `AppUIElements` will be ready to\nbe interacted with as states are changed. If the wait time is `nil`, then the default wait time will be 5 seconds.\n\nIf any of the specified UI elements are not currently visible, the `populate_data_fields` method will attempt to scroll\nthe UI object in view on the vertical axis (down, then up).\n```ruby\n    def enter_data(user_data)\n      fields = {\n        first_name_field   =\u003e user_data.first_name,\n        last_name_field    =\u003e user_data.last_name,\n        email_field        =\u003e user_data.email,\n        phone_number_field =\u003e user_data.phone_number\n      }\n      populate_data_fields(fields, wait_time = 2)\n    end\n```\n\n### Verifying AppUIElements on your ScreenObject or ScreenSection\n\nA typical automated test executes one or more interactions with the user interface, and then performs a validation to\nverify whether the expected state of the UI has been achieved. This verification can be performed using the various object\nstate methods(listed above) for each `AppUIElement` that requires verification. Depending on the complexity and number of\n`AppUIElements` to be verified, the code required to verify the presence of `AppUIElements` and their correct states can\nbecome cumbersome.\n\nThe `ScreenObject.verify_ui_states` and `ScreenSection.verify_ui_states` methods support the verification of multiple\nproperties of multiple UI elements on a `ScreenObject` or `ScreenSection`. The `verify_ui_states` method accepts a hash\ncontaining key/hash pairs of UI elements and their properties or attributes to be verified.\n```ruby\n     ui = {\n        object1 =\u003e { property: expected_state },\n        object2 =\u003e { property1: expected_state, property2: expected_state },\n        object3 =\u003e { property: expected_state }\n    }\n    verify_ui_states(ui)\n```\nThe `verify_ui_states` method automatically scrolls UI elements that are expected to be visible into view. Auto-scrolling\nonly occurs on the vertical axis (down, then up). Setting the `auto_scroll` parameter to `false` prevents automatic scrolling\nfrom occurring.\n\nThe `verify_ui_states` method queues up any exceptions that occur while verifying each object's properties until all\n`AppUIElements`and their properties have been checked, and then posts any exceptions encountered upon completion. Posted\nexceptions include a screenshot of the screen where expected results did not match actual results.\n\nThe `verify_ui_states` method supports the following property/state pairs:\n\n**All Objects:**\n\n    :exists     Boolean\n    :enabled    Boolean\n    :disabled   Boolean\n    :visible    Boolean\n    :hidden     Boolean\n    :width      Integer\n    :height     Integer\n    :x          Integer\n    :y          Integer\n    :count      Integer\n    :value      String\n    :caption    String\n    :attribute  Hash\n    :class      String\n\n**Text Fields:**\n\n    :placeholder String\n    :readonly    Boolean  (WebViews only)\n    :maxlength   Integer  (WebViews only)\n\n**Checkboxes and Switches:**\n\n    :checked Boolean\n\n**Radio Buttons:**\n\n    :selected Boolean\n\n**Lists and SelectLists**\n\n    :items     Array of Strings\n    :itemcount Integer\n\n**Menu Bars**\n\n    :items     Array of Strings\n    :itemcount Integer\n\n**Menus**\n\n    :items     Array of Strings\n    :itemcount Integer\n    :item_data Array of Hash\n\n**Tables**\n\n    :rowcount       Integer\n    :columncount    Integer\n    :columnheaders  Array of String\n    :cell           Hash\n    :row            Hash\n    :column         Hash\n\n#### Comparison States\n\nThe `verify_ui_states` method supports comparison states using property/comparison state pairs:\n\n    object =\u003e { property: { comparison_state: value } }\n\nComparison States:\n\n    :lt or :less_than                  Integer or String\n    :lt_eq or :less_than_or_equal      Integer or String\n    :gt or :greater_than               Integer or String\n    :gt_eq or :greater_than_or_equal   Integer or String\n    :starts_with                       String\n    :ends_with                         String\n    :contains                          String\n    :not_contains or :does_not_contain Integer or String\n    :not_equal                         Integer, String, or Boolean\n\n\n#### I18n Translation Validation\n\nThe `verify_ui_states` method also supports I18n string translations using property/I18n key name pairs:\n\n    object =\u003e { property: { translate_key: 'name of key in I18n compatible .yml file' } }\n\n**I18n Translation Keys:**\n\n    :translate            String\n    :translate_upcase     String\n    :translate_downcase   String\n    :translate_capitalize String\n    :translate_titlecase  String\n\nThe example below depicts the usage of the `verify_ui_states` method to verify that the captions for navigation menu items\nare correctly translated.\n\n![Localized UI](https://raw.githubusercontent.com/TestCentricity/testcentricity_apps/main/.github/images/LocalizedUI.png \"Localized UI\")\n```ruby\n    def verify_menu\n      ui = {\n              menu_title =\u003e {\n                visible: true,\n                caption: { translate: 'NavMenu.title' }\n              },\n              recipes_item =\u003e {\n                visible: true,\n                caption: { translate: 'NavMenu.recipes' }\n              },\n              browser_item =\u003e {\n                visible: true,\n                caption: { translate: 'NavMenu.browser' }\n              },\n              groceries_item =\u003e {\n                visible: true,\n                caption: { translate: 'NavMenu.groceries' }\n              },\n              pantry_item =\u003e {\n                visible: true,\n                caption: { translate: 'NavMenu.pantry' }\n              },\n              meals_item =\u003e {\n                visible: true,\n                caption: { translate: 'NavMenu.meals' }\n              },\n              menus_item =\u003e {\n                visible: true,\n                caption: { translate: 'NavMenu.menus' }\n              },\n              settings_item =\u003e {\n                visible: true,\n                caption: { translate: 'NavMenu.settings' }\n              }\n      }\n      verify_ui_states(ui)\n    end\n```\nI18n `.yml` files contain key/value pairs representing the name of a translated string (key) and the string value. For the\nmenu example above, the translated strings for English, Spanish, and French are represented below:\n\n**English** - `en.yml`\n```yaml\n    en:\n      NavMenu:\n        title: 'Main Menu'\n        recipes: 'Recipes'\n        browser: 'Browser'\n        groceries: 'Groceries'\n        pantry: 'Pantry'\n        meals: 'Meals'\n        menus: 'Menus'\n        settings: 'Settings'\n```\n**Spanish** - `es.yml`\n```yaml\n    es:\n      NavMenu:\n        title: 'Menú principal'\n        recipes: 'Recetas'\n        browser: 'Navegador'\n        groceries: 'Compra'\n        pantry: 'Despensa'\n        meals: 'Comidas'\n        menus: 'Menús'\n        settings: 'Ajustes'\n```\n**French** - `fr.yml`\n```yaml\n    fr:\n      NavMenu:\n        title: 'Menu principal'\n        recipes: 'Recettes'\n        browser: 'Navigateur'\n        groceries: 'Courses'\n        pantry: 'Provisions'\n        meals: 'Repas'\n        menus: 'Menus'\n        settings: 'Réglades'\n```\n\nEach supported language/locale combination has a corresponding `.yml` file. I18n `.yml` file naming convention uses\n[ISO-639 language codes and ISO-3166 country codes](https://docs.oracle.com/cd/E13214_01/wli/docs92/xref/xqisocodes.html). For example:\n\n| Language (Country)    | File name |\n|-----------------------|-----------|\n| English               | en.yml    |\n| English (Australia)   | en-AU.yml |\n| English (Canada)      | en-CA.yml |\n| French                | fr.yml    |\n| French (Canada)       | fr-CA.yml |\n| Spanish               | es.yml    |\n| German                | de.yml    |\n| Portuguese (Brazil)   | pt-BR.yml |\n| Portuguese (Portugal) | pt-PT.yml |\n\nBaseline translation strings are stored in `.yml` files in the `config/locales/` folder.\n\n       📁 my_automation_project/\n        ├── 📁 config/\n        │   ├── 📁 locales/\n        │   │   ├── 📄 en.yml\n        │   │   ├── 📄 en-AU.yml\n        │   │   ├── 📄 es.yml\n        │   │   ├── 📄 de.yml\n        │   │   ├── 📄 fr.yml\n        │   │   ├── 📄 fr-CA.yml\n        │   │   ├── 📄 pt-BR.yml\n        │   │   └── 📄 pt-PT.yml\n        │   ├── 📁 test_data/\n        │   └── 📄 cucumber.yml\n        ├── 📁 downloads/\n        ├── 📁 features/\n        ├── 📄 Gemfile\n        └── 📄 README.md\n\n\n### Working With Custom AppUIElements\n\n#### Vertical Scrolling ListView\n\n`AppUIElements` like ListViews (`AppList` class) are typically made up of multiple composite UI component types, which will\nbe different for iOS vs. Android mobile platforms. Below is an example of the vertical scrolling ListView implementations\nfor a cross-platform application implemented using React Native (iOS version on the left, Android version on the right).\nEach ListView contains 30 items:\n\n![Vertical Scrolling ListView](https://raw.githubusercontent.com/TestCentricity/testcentricity_apps/main/.github/images/ListViews.png \"Vertical Scrolling ListViews\")\n\nWhile the iOS and Android ListViews appear to be identical in the app, performing an inspection of each application's GUI\nusing Appium Inspector reveals differences in the object hierarchy as depicted below (iOS version on left, Android version\non the right):\n\n![Vertical Scrolling ListView Hierarchy](https://raw.githubusercontent.com/TestCentricity/testcentricity_apps/main/.github/images/ListHeirarchy.png \"Vertical Scrolling ListView Hierarchy\")\n\nThe inspection of the ListView object hierarchy reveals that for the iOS version of the app, list items are made up of\n`XCUIElementTypeOther` objects, and that for the Android version of the app, list items are made up of `android.view.ViewGroup`\nobjects.\n\nThe other, more notable difference is that while the iOS inspection shows all 30 list items, only 13 list items are shown\nin the inspection of the Android app, which corresponds to the list items that are visible on the Android device screen.\nWhen testing Android apps using the `UiAutomator2` driver for Appium, UI objects that are not displayed on screen cannot\nbe detected by Appium Inspector or by Appium based frameworks until the objects are scrolled into view.\n\nThe `AppList.define_list_elements` method provides a means of specifying the objects that make up the list item components\nof an `AppList` control, and the axis in which scrolling of the list items occurs. The method accepts a hash that can contain\nup to two key-value pairs. Valid key designators are `:list_item`and `:scrolling`. The `AppList.define_list_elements` method\nis typically called in the `initialize` method of the `ScreenObject` or `ScreenSection` that contains the associated `AppList`\ncontrol.\n\nThe code snippets below demonstrate the use of the `AppList.define_list_elements` method in the `CloudListScreen` screen\nobject's `initialize` method to define the list item components that make up the Clouds vertical scrolling ListView from\nthe above examples. It is not necessary to specify the scroll axis in the code below, as `:vertical` is the default scroll\naxis that is set when instantiating an `AppList` element.\n\niOS Cloud List `ScreenObject`\n```ruby\n    class CloudListScreen \u003c TestCentricity::ScreenObject\n      trait(:screen_name)    { 'Cloud List' }\n      trait(:screen_locator) { { class_chain: '**/XCUIElementTypeWindow/XCUIElementTypeOther/XCUIElementTypeOther/XCUIElementTypeOther' } }\n    \n      # Cloud List screen UI elements\n      list :cloud_list, { class_chain: '**/XCUIElementTypeScrollView/XCUIElementTypeOther' }\n    \n      def initialize\n        super\n        # define the list item element for the Cloud list object\n        list_spec = { list_item: { class: 'XCUIElementTypeOther' } }\n        cloud_list.define_list_elements(list_spec)\n      end\n    end\n```\n\nAndroid CloudListScreen `ScreenObject`\n```ruby\n    class CloudListScreen \u003c TestCentricity::ScreenObject\n      trait(:screen_name)    { 'Cloud List' }\n      trait(:screen_locator) { { xpath: '//android.widget.FrameLayout[@resource-id=\"android:id/content\"]/android.view.ViewGroup' } }\n    \n      # Cloud List screen UI elements\n      list :cloud_list, { xpath: '//android.widget.ScrollView/android.view.ViewGroup' }\n    \n      def initialize\n        super\n        # define the list item element for the Cloud list object\n        list_spec = { list_item: { class: 'android.view.ViewGroup' } }\n        cloud_list.define_list_elements(list_spec)\n      end\n    end\n```\n\n\n#### Horizontal Scrolling ListView\n\nBelow is an example of a horizontal scrolling \"Carousel\" style ListView implementations on the Swipe screen of a cross-platform\napplication. Each ListView contains 6 list items.\n\n![Horizontal Scrolling Carousel ListView](https://raw.githubusercontent.com/TestCentricity/testcentricity_apps/main/.github/images/Carousel.png \"Horizontal Scrolling Carousel ListViews\")\n\nWhile the iOS and Android ListViews appear to be identical in the app, performing an inspection of each application's GUI\nusing Appium Inspector reveals differences in the object hierarchy as depicted below (iOS version on left, Android version\non the right):\n\n![Horizontal Scrolling Carousel Hierarchy](https://raw.githubusercontent.com/TestCentricity/testcentricity_apps/main/.github/images/CarouselHierarchy.png \"Horizontal Scrolling Carousel Hierarchy\")\n\nAs in the previous example for the vertical scrolling ListView, the inspection of the Carousel ListView object hierarchy\nreveals that for the iOS version of the app, list items are again made up of `XCUIElementTypeOther` objects, and that for\nthe Android version of the app, list items are again made up of `android.view.ViewGroup` objects.\n\nAs in the previous examples, the iOS inspection shows all 6 list items, while only 2 list items are shown in the inspection\nof the Android app, which corresponds to the list items that are visible on the Android device screen.\n\nThe code snippets below demonstrate the use of the `AppList.define_list_elements` method in the `SwipeScreen` screen object's\n`initialize` method to define the scroll axis and list item components that make up the Carousel horizontal scrolling ListView\nfrom the above examples.\n\niOS Swipe `ScreenObject`\n```ruby\n    class SwipeScreen \u003c TestCentricity::ScreenObject\n      trait(:screen_name)    { 'Swipe' }\n      trait(:screen_locator) { { accessibility_id: 'Swipe-screen' } }\n    \n      # Swipe screen UI elements\n      list :carousel_list, { accessibility_id: 'Carousel' }\n    \n      def initialize\n        super\n        # define the list item element for the Carousel list object\n        list_spec = {\n          list_item: { xpath: '//XCUIElementTypeOther[contains(@name, \"__CAROUSEL_ITEM_\")]' },\n          scrolling: :horizontal\n        }\n        carousel_list.define_list_elements(list_spec)\n      end\n    end\n```\n\nAndroid Swipe `ScreenObject`\n```ruby\n    class SwipeScreen \u003c TestCentricity::ScreenObject\n      trait(:screen_name)    { 'Swipe' }\n      trait(:screen_locator) { { accessibility_id: 'Swipe-screen' } }\n    \n      # Swipe screen UI elements\n      list :carousel_list, { accessibility_id: 'Carousel' }\n    \n      def initialize\n        super\n        # define the list item element for the Carousel list object\n        list_spec = {\n          list_item: { xpath: '//android.view.ViewGroup[contains(@resource-id, \"__CAROUSEL_ITEM_\")]' },\n          scrolling: :horizontal\n        }\n        carousel_list.define_list_elements(list_spec)\n      end\nend\n```\n\n\n#### Popup and PickerWheel Style ListViews\n\nBelow is an example of a PickerWheel (iOS) and Popup (Android) style ListView implementations on the Form Components screen\nof a cross-platform application.\n\n![PickerWheel and Popup ListViews](https://raw.githubusercontent.com/TestCentricity/testcentricity_apps/main/.github/images/Popup_Picker.png \"PickerWheel and Popup ListViews\")\n\nPerforming an inspection of each application's GUI using Appium Inspector reveals differences in the object hierarchy as\ndepicted below (iOS version on left, Android version on the right):\n\n![PickerWheel and Popup ListView Hierarchy](https://raw.githubusercontent.com/TestCentricity/testcentricity_apps/main/.github/images/PopupHeirarchy.png \"PickerWheel and Popup ListView Hierarchy\")\n\nThe inspection of the PickerWheel and Popup ListView object hierarchies reveals that for the iOS version of the app, list\nitems are again made up of `XCUIElementTypeOther` objects, and that for the Android version of the app, list items are made\nup of `android.widget.CheckedTextView` objects.\n\nHowever, `XCUIElementTypePickerWheel` controls present testability challenges with Appium, as the `XCUIElementTypeOther`\nobjects that comprise the individual list items cannot be reliably interacted with or validated. When inspecting each of\nthe `XCUIElementTypeOther` list items of the `XCUIElementTypePickerWheel` control, there are no `text`, `accessibility_id`,\n`label`, or `value` element attributes available which could be used to determine whether the correct caption strings are\ndisplayed for each list item. The `AppList.get_item_count` or `get_list_items` methods do not support `XCUIElementTypePickerWheel`\ncontrols, and will raise an exception if called for such a control.\n\nFor the Android version of the app, the `android.widget.CheckedTextView` list items can be interacted with and validated,\nas the `text` element attribute for each list item are visible in the inspection.\n\nThe code snippet below demonstrate the use of the `AppList.define_list_elements` method in the `FormScreen` screen object's\n`initialize` method to define the list item components that make up the Android Popup style ListView from the above example.\n\nAndroid FormScreen `ScreenObject`\n```ruby\n    class FormScreen \u003c TestCentricity::ScreenObject\n      trait(:screen_name)    { 'Form' }\n      trait(:screen_locator) { { accessibility_id: 'Forms-screen' } }\n    \n      # Form screen UI elements\n      list :drop_down_menu, { id: 'com.wdiodemoapp:id/select_dialog_listview' }\n    \n      def initialize\n        super\n        # define the list item element for the drop-down list object\n        list_spec = { list_item: { class: 'android.widget.CheckedTextView' } }\n        drop_down_menu.define_list_elements(list_spec)\n      end\n    end\n```\n\n#### Table AppUIElements\n\nThe basic iOS or MacOS `AppTable` element is typically composed of the parent `XCUIElementTypeTable` object, containing\none or more rows (`XCUIElementTypeTableRow`), and containing one or more columns (`XCUIElementTypeTableColumn`). Tables\ncan also include an optional header (`XCUIElementTypeGroup`) containing one or more header cells (`XCUIElementTypeButton`).\n\nTable cells are typically embedded in each parent row object, and are implemented using a parent `XCUIElementTypeCell`\nwith a child `XCUIElementTypeStaticText` object that provides access to the text content of the table cell.\n\nAs such, when an iOS or MacOS `AppTable` element is instantiated in a `ScreenObject` or `ScreenSection`, the table's component\nobjects and their locator strategies are defined as follows:\n```ruby\n  {\n    table_row:         { class: 'XCUIElementTypeTableRow' },\n    table_column:      { class: 'XCUIElementTypeTableColumn' },\n    table_cell:        { class_chain: '**/XCUIElementTypeCell/XCUIElementTypeStaticText' },\n    table_header:      { class: 'XCUIElementTypeGroup' },\n    table_header_cell: { class: 'XCUIElementTypeButton' }\n  }\n```\n\nAn example of this table implementation in the MacOS Shortcuts app, and its object hierarchy as viewed by Appium Inspector,\nis depicted below:\n\n![Shortcuts Table](https://raw.githubusercontent.com/TestCentricity/testcentricity_apps/main/.github/images/Shortcuts.png \"Shortcuts Table\")\n\nThe above table object hierarchy for the Shortcuts table shows that access to the text content of the table's cells will\nfail using the typical implementation of `XCUIElementTypeCell/XCUIElementTypeStaticText` cell objects. This is due to the\npresence of an `XCUIElementTypeGroup` object between the `XCUIElementTypeCell` parent object and the `XCUIElementTypeStaticText`\ncell text caption object.\n\nThe `AppTable.define_table_elements` method provides a means of specifying the various elements that make up the key\ncomponents of an `AppTable` object, or for overriding the default component locator strategy. The method accepts a hash\nof table component designators (key) and a locator strategy (value) for identifying the component in the `AppTable` object\nhierarchy.\n\nValid table component designators are:\n* `table_row:`\n* `table_column:`\n* `table_cell:`\n* `table_header:`\n* `table_header_cell:`\n\nValid component `locator_strategy` are:\n* `class:`\n* `xpath:`\n* `predicate:` (MacOS and iOS only)\n* `class_chain:` (MacOS and iOS only)\n\nIf you were developing automated tests for the MacOS Shortcuts app, your `ShortcutsAppScreen` screen object's `initialize`\nmethod would use the `AppTable.define_table_elements` method to define the locator strategy for the `table_cell` component.\n```ruby\n    class ShortcutsAppScreen \u003c TestCentricity::ScreenObject\n      trait(:screen_name)    { 'Shortcuts' }\n      trait(:screen_locator) { { class_chain: '**/XCUIElementTypeWindow' } }\n    \n      # Shortcuts screen UI elements\n      tables sidebar_table: { predicate: 'identifier == \"library.sidebar\"' },\n             view_table:    { predicate: 'identifier == \"view.library.table\"' }\n    \n      def initialize\n        super\n        # define the cell element for the Shortcuts table object\n        table_spec = { table_cell: { class_chain: '**/XCUIElementTypeCell/XCUIElementTypeGroup/XCUIElementTypeStaticText' } }\n        view_table.define_table_elements(table_spec)\n      end\n    end\n```\n\nAnother `AppTable` implementation using elements other than the default `AppTable` components can be found in the MacOS\nActivity Monitor app (see below). An inspection of the Activity Monitor's table object hierarchy for the app reveals that\nit is an `XCUIElementTypeOutline` object comprised of `XCUIElementTypeOutlineRow` elements representing the table's rows.\n\n![Activity Monitor Table](https://raw.githubusercontent.com/TestCentricity/testcentricity_apps/main/.github/images/ActivityMonitor.png \"Activity Monitor Table\")\n\nThe `ActivityMonitorScreen` screen object's `initialize` method in the code snippet below demonstrates the use of the\n`AppTable.define_table_elements` method to define the locator strategy for the `table_row` component.\n```ruby\n    class ActivityMonitorScreen \u003c TestCentricity::ScreenObject\n      trait(:screen_name)    { 'Activity Monitor' }\n      trait(:screen_locator) { { class_chain: '**/XCUIElementTypeWindow' } }\n    \n      # Shortcuts screen UI elements\n      table :process_table, { class_chain: '**/XCUIElementTypeScrollView/XCUIElementTypeOutline' }\n    \n      def initialize\n        super\n        # define the row element for the Activity Monitor table object\n        table_spec = { table_row: { class: 'XCUIElementTypeOutlineRow' } }\n        process_table.define_table_elements(table_spec)\n      end\n    end\n```\n\n\n---\n## MacOS Application Menu Bar and Menus\n\nWith MacOS desktop applications, the menu bar at the top of the screen displays the top-level menus associated with your\napp, and typically includes both system-provided menus and app-specific menus. Below is the menu bar and **View** menu\nassociated with the MacOs Calculator application:\n\n![MacOS Calculator App MenuBar](https://raw.githubusercontent.com/TestCentricity/testcentricity_apps/main/.github/images/MacOS_Menus.png \"MacOS Calculator App MenuBar\")\n\nPerforming an inspection of the Calculator application's GUI using Appium Inspector reveals that the `XCUIElementTypeMenuBar`\nobject is not a child object of the Calculator app's main `XCUIElementTypeWindow`, but resides at the top level of the\nMacOS app object hierarchies, along with the app's windows.\n\n![Calculator App MenuBar Hierarchy](https://raw.githubusercontent.com/TestCentricity/testcentricity_apps/main/.github/images/MenuBarHierarchy.png \"Calculator App MenuBar Hierarchy\")\n\n\n### Defining a MenuBar\n\nTestCentricity For Apps provides a `MenuBar` class object, that is a special subclass of the `ScreenSection` class object.\nYou define a new `MenuBar` as shown below:\n```ruby\n    class CalculatorMenuBar \u003c TestCentricity::MenuBar\n    end\n```\n\n### Adding a MenuBar to your App's Primary ScreenObject\n\nYou add a `MenuBar` to your app's primary `ScreenObject` as shown below:\n```ruby\n    class CalculatorAppScreen \u003c TestCentricity::ScreenObject\n      # Calculator App screen UI elements\n      menubar :calc_menu_bar, CalculatorMenuBar\n    end\n```\nOnce your `ScreenObject` has been instantiated, you can call its `MenuBar` methods as shown below:\n```ruby\n    num_menus = calculator_screen.calc_menu_bar.get_item_count\n```\n\n### Adding Menus to your MenuBar\n\nA `MenuBar` is typically made up of one or more `Menu` objects, which are added to your `MenuBar` class definition as shown\nbelow:\n```ruby\n    class CalculatorMenuBar \u003c TestCentricity::MenuBar\n      # Calculator Menu Bar UI elements\n      menus calc_menu:        { class_chain: '**/XCUIElementTypeMenuBarItem[2]' },\n            file_menu:        { class_chain: '**/XCUIElementTypeMenuBarItem[3]' },\n            edit_menu:        { class_chain: '**/XCUIElementTypeMenuBarItem[4]' },\n            view_menu:        { class_chain: '**/XCUIElementTypeMenuBarItem[5]' },\n            view_sub_menu:    { predicate: 'identifier == \"_NS:335\"' },\n            convert_menu:     { class_chain: '**/XCUIElementTypeMenuBarItem[6]' },\n            convert_sub_menu: { predicate: 'identifier == \"_NS:352\"' },\n            speech_menu:      { class_chain: '**/XCUIElementTypeMenuBarItem[7]' },\n            window_menu:      { class_chain: '**/XCUIElementTypeMenuBarItem[8]' },\n            help_menu:        { class_chain: '**/XCUIElementTypeMenuBarItem[9]' }\n    end\n```\n\n### Adding Methods to your MenuBar\n\nYou can add methods to your `MenuBar` class definition, as shown below:\n```ruby\n    class CalculatorMenuBar \u003c TestCentricity::MenuBar\n      # Calculator Menu Bar UI elements\n      menus calc_menu:        { class_chain: '**/XCUIElementTypeMenuBarItem[2]' },\n            file_menu:        { class_chain: '**/XCUIElementTypeMenuBarItem[3]' },\n            edit_menu:        { class_chain: '**/XCUIElementTypeMenuBarItem[4]' },\n            view_menu:        { class_chain: '**/XCUIElementTypeMenuBarItem[5]' },\n            view_sub_menu:    { predicate: 'identifier == \"_NS:335\"' },\n            convert_menu:     { class_chain: '**/XCUIElementTypeMenuBarItem[6]' },\n            convert_sub_menu: { predicate: 'identifier == \"_NS:352\"' },\n            speech_menu:      { class_chain: '**/XCUIElementTypeMenuBarItem[7]' },\n            window_menu:      { class_chain: '**/XCUIElementTypeMenuBarItem[8]' },\n            help_menu:        { class_chain: '**/XCUIElementTypeMenuBarItem[9]' }\n    \n      def choose_menu_item(menu, item, method = :mouse)\n        menu_map = {\n          calc:             calc_menu,\n          calculator:       calc_menu,\n          file:             file_menu,\n          edit:             edit_menu,\n          view:             view_menu,\n          view_sub_menu:    view_sub_menu,\n          convert:          convert_menu,\n          convert_sub_menu: convert_sub_menu,\n          window:           window_menu\n        }\n        menu_obj = menu_map[menu]\n        raise \"#{menu} is not a supported menu\" if menu_obj.nil?\n        menu_obj.choose_menu_item(item, method)\n      end\n    \n      def verify_menu_bar\n        ui = {\n                self =\u003e {\n                  enabled: true,\n                  items: %w[Apple Calculator File Edit View Convert Speech Window Help],\n                  itemcount: 9\n                },\n                calc_menu =\u003e {\n                  enabled: true,\n                  itemcount: 8,\n                  item_data: [\n                    { caption: 'About Calculator', enabled: true },\n                    { caption: '', enabled: false },\n                    { caption: 'Hide Calculator', enabled: true },\n                    { caption: 'Hide Others', enabled: true },\n                    { caption: 'Show All', enabled: false },\n                    { caption: '', enabled: false },\n                    { caption: 'Quit Calculator', enabled: true },\n                    { caption: 'Quit and Keep Windows', enabled: true }\n                  ]\n                },\n                speech_menu =\u003e {\n                  enabled: true,\n                  itemcount: 2,\n                  items: ['Speak Button Pressed', 'Speak Result']\n                }\n        }\n        verify_ui_states(ui)\n      end\n    end\n```\n\n### Defining Keyboard Shortcuts for your App's Menus\n\nThe Appium Mac2 Driver does not support element attributes that could be used to verify the checked state of a menu item\nor the keyboard shortcut assigned to a menu item. However, it is possible to verify that menu item keyboard shortcuts are\nfunctional by mapping the keyboard shortcuts to a particular menu item and then calling `AppMenu.choose_menu_item` with\nthe `method` parameter set to `:keys` or `:keyboard` and then verifying that the expected menu-triggered action occurs.\n\nThe `AppMenu.define_menu_elements` method provides a means of specifying the objects that make up the menu item components\nof an `AppMenu` control, and the keyboard shortcut map for each menu item in a menu. The method accepts a hash that can\ncontain up to two key-value pairs. Valid key designators are `:menu_item` and `:key_map`.\n\nWhen `AppMenu` objects are instantiated, the `:menu_item` attribute is set to the `XCUIElementTypeMenuItem` class, which\nis the default menu item object for MacOS applications. The `AppMenu.define_menu_elements` method is typically called in\nthe `initialize` method of your app's `MenuBar` control.\n\nThe code snippet below demonstrate the use of the `AppMenu.define_menu_elements` method in the `CalculatorMenuBar` object's\n`initialize` method to define the keyboard shortcut mapping for 4 of the menu items in the **View** menu and 1 of the menu\nitems in the **Window** menu of the MacOS Calculator app. Keyboard shortcuts are assigned to the **View** menu items by\nindex (menu items 1, 2, 3, and 7) and to the **Window** menu by menu item caption (menu item *Show Paper Tape*).\n\n```ruby\n    class CalculatorMenuBar \u003c TestCentricity::MenuBar\n\n      def initialize(name, parent, locator, context)\n        super\n        # define key map for View menu\n        menu_items = {\n          key_map: {\n            1 =\u003e [key: '1', modifierFlags: XCUIKeyModifierCommand],\n            2 =\u003e [key: '2', modifierFlags: XCUIKeyModifierCommand],\n            3 =\u003e [key: '3', modifierFlags: XCUIKeyModifierCommand],\n            7 =\u003e [key: 'r', modifierFlags: XCUIKeyModifierCommand]\n          }\n        }\n        view_menu.define_menu_elements(menu_items)\n        # define key map for Window menu\n        menu_items = {\n          key_map: { 'Show Paper Tape' =\u003e [key: 't', modifierFlags: XCUIKeyModifierCommand] }\n        }\n        window_menu.define_menu_elements(menu_items)\n      end\n    end\n```\nThe `modifierFlags` argument is an `unsigned long` type and defines the bitmask with depressed modifier keys for the given\nkey. TestCentricity and XCTest defines the following possible bitmasks for modifier keys:\n```ruby\n    XCUIKeyModifierNone     = 0\n    XCUIKeyModifierCapsLock = (1 \u003c\u003c 0)\n    XCUIKeyModifierShift    = (1 \u003c\u003c 1)\n    XCUIKeyModifierControl  = (1 \u003c\u003c 2)\n    XCUIKeyModifierOption   = (1 \u003c\u003c 3)\n    XCUIKeyModifierCommand  = (1 \u003c\u003c 4)\n    XCUIKeyModifierFunction = (1 \u003c\u003c 5)\n```\nRefer to [this page](https://github.com/appium/appium-mac2-driver?tab=readme-ov-file#macos-keys) for more information on MacOS keyboard `modifierFlags`.\n\n### Adding Popup Menus to your ScreenObject or ScreenSection\n\nA MocOS desktop app may have one or more popup menus that can appear on a `ScreenObject` or `ScreenSection`. Below is an\nexample of two different popup menus that can be invoked on the MacOS Shortcuts app - one on sidebar list items, and the\nother on Shortcuts table items.\n\n![Popup Menus](https://raw.githubusercontent.com/TestCentricity/testcentricity_apps/main/.github/images/PopupMenus.jpg \"Popup Menus\")\n\nIt is typically not necessary to define more than one popup menu object in your `ScreenObject` or `ScreenSection` class\ndefinition, even if there are multiple popups that can be interacted with. Since only one popup can be visible at a time,\nyou only need to define a single popup menu object.\n\nPopup menus are added to your `ScreenObject` or `ScreenSection` class definition as shown below:\n```ruby\n    class ShortcutsAppScreen \u003c TestCentricity::ScreenObject\n      # Shortcuts App screen UI elements\n      menu    :popup_menu, { class_chain: '**/XCUIElementTypeTable/XCUIElementTypeMenu' }\n      menubar :shortcuts_menu_bar, ShortcutsMenuBar\n    end\n```\n\n\n---\n## Instantiating ScreenObjects and Utilizing the ScreenManager\n\nBefore you can call the methods in your `ScreenObjects` and `ScreenSections`, you must instantiate the `ScreenObjects` of\nyour MacOS dektop app or native mobile application, as well as create instance variables which can be used when calling\n`ScreenObject` methods from your step definitions or specs.\n\nThe `ScreenManager` class provides methods for supporting the instantiation and management of `ScreenObjects`. In the code\nexample below, the `screen_objects` method contains a hash table of your `ScreenObject` instances and their associated\n`ScreenObject` classes to be instantiated by `ScreenManager`:\n```ruby\n    module WorldScreens\n      def screen_objects\n        {\n          login_screen:            LoginScreen,\n          registration_screen:     RegistrationScreen,\n          search_results_screen:   SearchResultsScreen,\n          products_grid_screen:    ProductsCollectionScreen,\n          product_detail_screen:   ProductDetailScreen,\n          shopping_basket_screen:  ShoppingBasketScreen,\n          payment_method_screen:   PaymentMethodScreen,\n          confirm_purchase_screen: PurchaseConfirmationScreen,\n          my_account_screen:       MyAccountScreen,\n          my_order_history_screen: MyOrderHistoryScreen\n        }\n      end\n    end\n    \n    World(WorldScreens)\n```\n\nThe `WorldScreens` module above should be defined in the `world_screens.rb` file in the `features/support` folder.\n\nInclude the code below in your `env.rb` file to ensure that your `ScreenObjects` are instantiated before your Cucumber\nscenarios are executed:\n```ruby\n    include WorldScreens\n    WorldPages.instantiate_screen_objects\n```\n**NOTE:** If you intend to use the `ScreenManager`, you must define a `screen_name` trait for each of the `ScreenObjects`\nto be registered.\n\n\n### Leveraging the ScreenManager in your Cucumber tests\n\nMany Cucumber based automated tests suites include scenarios that verify that mobile app screens are correctly loaded,\ndisplayed, or can be navigated to by clicking associated menus and navigation elements. One such Cucumber navigation\nscenario is displayed below:\n```gherkin\n    Scenario Outline:  Verify screen navigation features\n      Given I am on the Products screen\n      When I tap the \u003cscreen_name\u003e navigation menu item\n      Then I expect the \u003cscreen_name\u003e screen to be correctly displayed\n        \n      Examples:\n        |screen_name      |\n        |Registration     |\n        |Shopping Basket  |\n        |My Account       |\n        |My Order History |\n```\nIn the above example, the step definitions associated with the 3 steps can be implemented using the `ScreenManager.find_screen`\nmethod to match the specified `screen_name` argument with the corresponding `ScreenObject` as shown below:\n```ruby\n    include TestCentricity\n\n    When(/^I (?:load|am on) the (.*) screen$/) do |screen_name|\n      # find and load the specified target screen\n      target_screen = ScreenManager.find_screen(screen_name)\n      target_screen.load_screen\n    end\n    \n    \n    When(/^I (?:click|tap) the ([^\\\"]*) navigation menu item$/) do |screen_name|\n      # find and navigate to the specified target screen\n      target_screen = ScreenManager.find_screen(screen_name)\n      target_screen.navigate_to\n    end\n    \n    \n    Then(/^I expect the (.*) screen to be correctly displayed$/) do |screen_name|\n      # find and verify that the specified target screen is loaded\n      target_screen = ScreenManager.find_screen(screen_name)\n      target_screen.verify_screen_exists\n      # verify that target screen is correctly displayed\n      target_screen.verify_screen_ui\n    end\n```\n\n\n---\n## Connecting to a MacOS Desktop Application\n\nThe `TestCentricity::AppiumConnect.initialize_appium` method configures the appropriate Appium capabilities required to\nestablish a connection with a locally hosted MacOS desktop application. The `initialize_appium` method accepts an `options`\nhash for specifying desired capabilities (using the W3C protocol), driver type, driver name, and endpoint URL information.\n\nThe following options and capabilities must be specified in the `options` hash:\n- `driver:` must be set to `:appium`\n- `platformName:` must be set to `:mac` in the `capabilities:` hash\n- `'appium:automationName':` must be set to `mac2` in the `capabilities:` hash\n- `'appium:bundleId'`: must be set to the Bundle ID of the MacOS app to be tested\n\n```ruby\n    options = {\n      driver: :appium,\n      capabilities: {\n        platformName: :mac,\n        'appium:automationName': 'mac2',\n        'appium:bundleId': 'com.apple.calculator'\n      }\n    }\n    AppiumConnect.initialize_appium(options)\n```\nAdditional options that can be specified in an `options` hash include the following:\n\n| Option           | Purpose                                                                        |\n|------------------|--------------------------------------------------------------------------------|\n| `driver_name:`   | optional driver name                                                           |\n| `endpoint:`      | optional endpoint URL for local Appium server or cloud hosted service provider |\n| `global_driver:` | define new driver with global scope if `true`                                  |\n\nRefer to [this page](https://github.com/appium/appium-mac2-driver#capabilities) for information regarding specifying Appium capabilities that are\nspecific to the Mac2 Driver driver.\n\nThe Appium server must be running prior to invoking Cucumber to run your features/scenarios on a locally hosted MacOS desktop\napplication. Refer to [**Section 10.2.3 (Starting and Stopping Appium Server)**](#starting-and-stopping-appium-server) below.\n\n---\n## Connecting to an iOS or Android Mobile Simulator or Device\n\nThe `AppiumConnect.initialize_appium` method configures the appropriate Appium capabilities required to establish a connection\nwith a locally or cloud hosted target iOS or Android simulator or real device.\n\nSince its inception, TestCentricity has provided support for establishing a single connection to a target iOS or Android\nsimulator or real device by instantiating an Appium driver object. **Environment Variables** are used to specify the local\nor remote cloud hosted target platform, and the various Appium capability parameters required to configure the driver object.\nThe appropriate **Environment Variables** are typically specified in the command line at runtime through the use of profiles\nset in a `cucumber.yml` file (Refer to [**Section 10.4 (Using Configuration Specific Profiles in `cucumber.yml`)**](#using-configuration-specific-profiles-in-cucumber-yml) below).\n\nHowever, due to the growing number of optional Appium capabilities that are being offered by cloud hosted service providers\n(like BrowserStack, Sauce Labs, TestingBot, or LambdaTest), **Environment Variables** may not effectively address.\n\nBeginning with TestCentricity version 4.0.0, the `TestCentricity::AppiumConnect.initialize_appium` method accepts an optional\n`options` hash for specifying desired capabilities (using the W3C protocol), driver type, driver name, endpoint URL, and\ndevice type information.\n\n\n### Specifying Options and Capabilities in the `options` Hash\n\nFor those test scenarios where cumbersome **Environment Variables** are less than ideal, call the `AppiumConnect.initialize_appium`\nmethod with an `options` hash that specifies the Appium desired capabilities, the driver type, and the device type, as depicted\nin the example below:\n```ruby\n    options = {\n      driver: :appium,\n      devicetype: :phone or :tablet,\n      capabilities: {\n        platformName: :ios or :android,\n        'appium:platformVersion': os_version,\n        'appium:deviceName': device_name,\n        'appium:automationName': 'XCUITest' or 'UiAutomator2',\n        'appium:app': path_to_app\n      }\n    }\n    AppiumConnect.initialize_appium(options)\n```\nAdditional options that can be specified in an `options` hash include the following:\n\n| Option           | Purpose                                                                        |\n|------------------|--------------------------------------------------------------------------------|\n| `driver_name:`   | optional driver name                                                           |\n| `endpoint:`      | optional endpoint URL for local Appium server or cloud hosted service provider |\n| `global_driver:` | define new driver with global scope if `true`                                  |\n\n\nDetails on specifying desired capabilities, driver type, endpoint URL, global driver scope, and default driver names are\nprovided in each of the platform hosting sections below.\n\n#### Specifying the Driver Type\n\nThe `driver:` type is a required entry in the `options` hash when instantiating an Appium driver object using the\n`initialize_appium` method. Valid `driver:` type values are listed in the table below:\n\n| `driver:`       | **Driver Type**                                                       |\n|-----------------|-----------------------------------------------------------------------|\n| `:appium`       | locally hosted native iOS/Android device simulator or physical device |\n| `:browserstack` | remote hosted on BrowserStack                                         |\n| `:saucelabs`    | remote hosted on Sauce Labs                                           |\n| `:testingbot`   | remote hosted on TestingBot                                           |\n| `:custom`       | remote hosted on unsupported cloud based hosting services             |\n\n#### Specifying a Driver Name\n\nAn optional user defined `driver_name:` can be specified in the `options` hash when instantiating an Appium driver object\nusing the `AppiumConnect.initialize_appium` method. If a driver name is not specified, the `initialize_appium` method will\nassign a default driver name comprised of the specified driver type (`driver:`) and the device OS and device type specified\nin the `capabilities:` hash. Details on default driver names are provided in each of the device/simulator hosting sections\nbelow.\n\n\n### Connecting to Locally Hosted Simulators or Physical Devices\n\nRefer to [this page](https://appium.io/docs/en/2.6/guides/caps/) for information regarding specifying Appium capabilities. The Appium server must be running prior\nto invoking Cucumber to run your features/scenarios on locally hosted iOS or Android simulators or physical devices. Refer\nto [**Section 10.2.3 (Starting and Stopping Appium Server)**](#starting-and-stopping-appium-server) below.\n\n⚠️ If you are running locally hosted mobile tests on iOS or Android simulators or devices using version 1.x of the Appium\nserver, the `APPIUM_SERVER_VERSION` environment variable must be set to `1` in order to ensure that the correct Appium server\nendpoint is used.\n\n#### Connecting to Locally Hosted iOS Simulators or Physical Devices\n\nYou can run your automated tests on locally hosted iOS simulators or physically connected devices using Appium and XCode\non macOS. You must install Appium, XCode, and the iOS version-specific device simulators for XCode. Information about\nAppium setup and configuration requirements with the XCUITest driver for testing on physically connected iOS devices can\nbe found on [this page](https://github.com/appium/appium-xcuitest-driver/blob/master/docs/preparation/real-device-config.md). Refer to [this page](https://appium.github.io/appium-xcuitest-driver/latest/capabilities/) for information regarding specifying Appium capabilities that are\nspecific to the XCUITest driver.\n\n##### Local iOS Simulators or Physical Devices using Environment Variables\n\nIf the `options` hash is not provided when calling the `TestCentricity::AppiumConnect.initialize_appium` method, the following\n**Environment Variables** must be set as described in the table below.\n\n| **Environment Variable** | **Description**                                                                                                                                                       |\n|--------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| `DRIVER`                 | Must be set to `appium`                                                                                                                                               |\n| `APP_PLATFORM_NAME`      | Must be set to `iOS`                                                                                                                                                  |\n| `AUTOMATION_ENGINE`      | Must be set to `XCUITest`                                                                                                                                             |\n| `APP_VERSION`            | Must be set to `17.4`, `16.2`, or which ever iOS version you wish to run within the XCode Simulator                                                                   |\n| `APP_DEVICE`             | Set to iOS device name supported by the iOS Simulator (`iPhone 13 Pro Max`, `iPad Pro (12.9-inch) (5th generation)`, etc.) or name of physically connected iOS device |\n| `DEVICE_TYPE`            | Must be set to `phone` or `tablet`                                                                                                                                    |\n| `APP`                    | Must be set to path where iOS app can be accessed and loaded                                                                                                          |\n| `UDID`                   | UDID of physically connected iOS device (not used for simulators)                                                                                                     |\n| `TEAM_ID`                | unique 10-character Apple developer team identifier string (not used for simulators)                                                                                  |\n| `TEAM_NAME`              | String representing a signing certificate (not used for simulators)                                                                                                   |\n| `APP_NO_RESET`           | [Optional] Don't reset app state after each test. Set to `true` or `false`                                                                                            |\n| `APP_FULL_RESET`         | [Optional] Perform a complete reset. Set to `true` or `false`                                                                                                         |\n| `WDA_LOCAL_PORT`         | [Optional] Used to forward traffic from Mac host to real iOS devices over USB. Default value is same as port number used by WDA on device.                            |\n| `LOCALE`                 | [Optional] Locale to set for the simulator.  e.g.  `fr_CA`                                                                                                            |\n| `LANGUAGE`               | [Optional] Language to set for the simulator.  e.g.  `fr`                                                                                                             |\n| `ORIENTATION`            | [Optional] Set to `portrait` or `landscape` (only for iOS simulators)                                                                                                 |\n| `NEW_COMMAND_TIMEOUT`    | [Optional] Time (in Seconds) that Appium will wait for a new command from the client                                                                                  |\n| `SHOW_SIM_KEYBOARD`      | [Optional] Show the simulator keyboard during text entry. Set to `true` or `false`                                                                                    |\n| `SHUTDOWN_OTHER_SIMS`    | [Optional] Close any other running simulators. Set to `true` or `false`. See note below.                                                                              |\n\nThe `SHUTDOWN_OTHER_SIMS` environment variable can only be set if you are running Appium Server with the `--relaxed-security`\nor `--allow-insecure=shutdown_other_sims` arguments passed when starting it from the command line, or when running the server\nfrom the Appium Server GUI app. A security violation error will occur without relaxed security enabled.\n\nRefer to [**Section 10.4 (Using Configuration Specific Profiles in `cucumber.yml`)**](#using-configuration-specific-profiles-in-cucumber-yml) below.\n\n\n##### Local iOS Simulators or Physical Devices using the `options` Hash\n\nWhen using the `options` hash, the following options and capabilities must be specified:\n- `driver:` must be set to `:appium`\n- `device_type:` must be set to `:tablet` or `:phone`\n- `platformName:` must be set to `ios` in the `capabilities:` hash\n- `'appium:automationName':` must be set to `xcuitest` in the `capabilities:` hash\n- `'appium:platformVersion':` must be set to the version of iOS on the simulator or physical device\n- `'appium:deviceName':` must be set to the name of the iOS simulator or physical device\n- `'appium:app'`: must be set to path where iOS app can be accessed and loaded\n\n```ruby\n    options = {\n      driver: :appium,\n      device_type: phone_or_tablet,\n      capabilities: {\n        platformName: :ios,\n        'appium:automationName': 'xcuitest',\n        'appium:platformVersion': ios_version,\n        'appium:deviceName': device_or_simulator_name,\n        'appium:app': path_to_ios_app\n      },\n      endpoint: 'http://127.0.0.1:4723/wd/hub'\n    }\n    AppiumConnect.initialize_appium(options)\n```\n\u003e ℹ️ If an optional user defined `driver_name:` is not specified in the `options` hash, the default driver name will be set to\n`appium_\u003cdevice_os\u003e_\u003cdevice_type\u003e` - e.g. `:appium_ios_phone` or `:appium_ios_tablet`.\n\u003e\n\u003e ℹ️ If an `endpoint:` is not specified in the `options` hash, then the default remote endpoint URL of `http://127.0.0.1:4723/wd/hub`\nwill be used.\n\u003e\n\u003e ℹ️ If `global_driver:` is not specified in the `options` hash, then the driver will be initialized without global scope.\n\nBelow is an example of an `options` hash for specifying a connection to a locally hosted mobile app running on an iPad Pro\nsimulator. The `options` hash includes options for specifying the driver name, global driver scope, and setting the simulated\ndevice orientation to portrait mode.\n```ruby\n    options = {\n      driver: :appium,\n      device_type: :tablet,\n      driver_name: :my_custom_ipad_driver,\n      global_driver: true,\n      capabilities: {\n        platformName: :ios,\n        'appium:platformVersion': '15.4',\n        'appium:deviceName': 'iPad Pro (12.9-inch) (5th generation)',\n        'appium:automationName': 'XCUITest',\n        'appium:orientation': 'PORTRAIT',\n        'appium:app': Environ.current.ios_app_path\n      }\n    }\n    AppiumConnect.initialize_appium(options)\n```\n\n#### Connecting to Locally Hosted Android Simulators or Physical Devices\n\nYou can run your automated tests on locally hosted Android simulators or physically connected devices using Appium and Android\nStudio on macOS. You must install Android Studio, the desired Android version-specific virtual device emulators, and\nAppium. Refer to [this page](https://appium.io/docs/en/2.6/quickstart/uiauto2-driver/) for information on configuring Appium to work with the Android SDK. Refer to [this page](https://github.com/appium/appium-uiautomator2-driver)\nfor information regarding specifying  Appium capabilities that are specific to the UiAutomator2 driver.\n\n##### Local Android Simulators or Physical Devices using Environment Variables\n\nIf the `options` hash is not provided when calling the `TestCentricity::AppiumConnect.initialize_appium` method, the following\n**Environment Variables** must be set as described in the table below.\n\n| **Environment Variable**  | **Description**                                                                                                                |\n|---------------------------|--------------------------------------------------------------------------------------------------------------------------------|\n| `DRIVER`                  | Must be set to `appium`                                                                                                        |\n| `APP_PLATFORM_NAME`       | Must be set to `Android`                                                                                                       |\n| `AUTOMATION_ENGINE`       | Must be set to `UiAutomator2`                                                                                                  |\n| `APP_VERSION`             | Must be set to `12.0`, or which ever Android OS version you wish to run with the Android Virtual Device                        |\n| `APP_DEVICE`              | Set to Android Virtual Device ID (`Pixel_2_XL_API_26`, `Nexus_6_API_23`, etc.) found in Advanced Settings of AVD Configuration |\n| `DEVICE_TYPE`             | Must be set to `phone` or `tablet`                                                                                             |\n| `APP`                     | Must be set to path where Android `.apk` file can be accessed and loaded                                                       || `UDID`                    | UDID of physically connected Android device (not used for simulators)                                                          |\n| `ORIENTATION`             | [Optional] Set to `portrait` or `landscape`                                                                                    |\n| `APP_NO_RESET`            | [Optional] Don't reset app state after each test. Set to `true` or `false`                                                     |\n| `APP_FULL_RESET`          | [Optional] Perform a complete reset. Set to `true` or `false`                                                                  |\n| `LOCALE`                  | [Optional] Locale to set for the simulator.  e.g.  `fr_CA`                                                                     |\n| `LANGUAGE`                | [Optional] Language to set for the simulator.  e.g.  `fr`                                                                      |\n| `NEW_COMMAND_TIMEOUT`     | [Optional] Time (in Seconds) that Appium will wait for a new command from the client                                           |\n| `CHROMEDRIVER_EXECUTABLE` | [Optional] Absolute local path to ChromeDriver executable                                                                      |\n\nRefer to [**Section 10.4 (Using Configuration Specific Profiles in `cucumber.yml`)**](#using-configuration-specific-profiles-in-cucumber-yml) below.\n\n\n##### Local Android Simulators or Physical Devices using the `options` Hash\n\nWhen using the `options` hash, the following options and capabilities must be specified:\n- `driver:` must be set to `:appium`\n- `device_type:` must be set to `:tablet` or `:phone`\n- `platformName:` must be set to `Android` in the `capabilities:` hash\n- `'appium:automationName':` must be set to `UiAutomator2` in the `capabilities:` hash\n- `'appium:platformVersion':` must be set to the version of Android on the simulator or physical device\n- `'appium:deviceName':` must be set to the Android Virtual Device ID\n- `'appium:app'`: must be set to path where Android `.apk` file can be accessed and loaded\n\n```ruby\n    options = {\n      driver: :appium,\n      device_type: phone_or_tablet,\n      capabilities: {\n        platformName: :android,\n        'appium:automationName': 'UiAutomator2',\n        'appium:platformVersion': android_version,\n        'appium:deviceName': simulator_name,\n        'appium:avd': simulator_name,\n        'appium:app': path_to_android_app\n      },\n      endpoint: 'http://localhost:4723/wd/hub'\n    }\n    AppiumConnect.initialize_appium(options)\n```\n\u003e ℹ️ If an optional user defined `driver_name:` is not specified in the `options` hash, the default driver name will be set to\n`appium_\u003cdevice_os\u003e_\u003cdevice_type\u003e` - e.g. `:appium_android_phone` or `:appium_android_tablet`.\n\u003e\n\u003e ℹ️ If an `endpoint:` is not specified in the `options` hash, then the default remote endpoint URL of ``http://127.0.0.1:4723/wd/hub``\nwill be used.\n\u003e\n\u003e ℹ️ If `global_driver:` is not specified in the `options` hash, then the driver will be initialized without global scope.\n\n\nBelow is an example of an `options` hash for specifying a connection to a locally hosted mobile app running on an Android\ntablet simulator. The `options` hash includes options for specifying the driver name and setting the simulated device orientation\nto landscape mode.\n```ruby\n      options = {\n        driver: :appium,\n        device_type: :tablet,\n        driver_name: :admin_tablet,\n        capabilities: {\n          platformName: 'Android',\n          'appium:platformVersion': '12.0',\n          'appium:deviceName': 'Pixel_C_API_31',\n          'appium:avd': 'Pixel_C_API_31',\n          'appium:automationName': 'UiAutomator2',\n          'appium:orientation': 'LANDSCAPE',\n          'appium:app': Environ.current.android_apk_path\n        }\n      }\n      AppiumConnect.initialize_appium(options)\n```\n#### Starting and Stopping Appium Server\n\n##### Using Appium Server with Cucumber\n\nThe Appium server must be running prior to invoking Cucumber to run your features/scenarios on locally hosted mobile simulators\nor physical devices. To programmatically control the starting and stopping of Appium server with the execution of your automated\ntests, place the code shown below in your `hooks.rb` file.\n```ruby\n    BeforeAll do\n      # start Appium Server if APPIUM_SERVER = 'run'\n      if ENV['APPIUM_SERVER'] == 'run'\n        $server = TestCentricity::AppiumServer.new\n        $server.start\n      end\n    end\n    \n    AfterAll do\n      # close Appium driver\n      TestCentricity::AppiumConnect.quit_driver\n      # terminate Appium Server if command line option was specified and Appium server is running\n      if ENV['APPIUM_SERVER'] == 'run' \u0026\u0026 Environ.driver == :appium \u0026\u0026 $server.running?\n        $server.stop\n      end\n    end\n```\nThe `APPIUM_SERVER` environment variable must be set to `run` in order to programmatically start and stop the Appium server.\nThis can be set by adding the following to your `cucumber.yml` file and including `-p run_appium` in your command line when\nstarting your Cucumber test suite(s):\n\n    run_appium: APPIUM_SERVER=run\n\nIf you are running locally hosted mobile tests on iOS or Android simulators or devices using version 1.x of the Appium server,\nthe `APPIUM_SERVER_VERSION` environment variable must be set to `1` in order to ensure that the correct Appium server endpoint\nis used. This can be set by adding the following to your `cucumber.yml` file and including `-p appium_1x` in your command line\nwhen starting your Cucumber test suite(s):\n\n    appium_1x: APPIUM_SERVER_VERSION=1\n\nRefer to [**Section 10.4 (Using Configuration Specific Profiles in `cucumber.yml`)**](#using-configuration-specific-profiles-in-cucumber-yml) below.\n\n\n##### Using Appium Server with RSpec\n\nThe Appium server must be running prior to executing test specs on locally hosted mobile simulators or physical device. To\ncontrol the starting and stopping of the Appium server with the execution of your specs, place the code shown below in the\nbody of an example group:\n```ruby\n    before(:context) do\n      # start Appium server before all of the examples in this group\n      $server = TestCentricity::AppiumServer.new\n      $server.start\n    end\n    \n    after(:context) do\n      # terminate Appium Server after all of the examples in this group\n      $server.stop if Environ.driver == :appium \u0026\u0026 $server.running?\n    end\n```\nIf you are running locally hosted mobile tests on iOS or Android simulators or devices using version 1.x of the Appium server,\nthe `APPIUM_SERVER_VERSION` environment variable must be set to `1` in order to ensure that the correct Appium server endpoint\nis used.\n\n\n###  Connecting to Remote Cloud Hosted iOS and Android Simulators or Physical Devices\n\nYou can run your automated tests against remote cloud hosted iOS and Android simulators and real devices using the BrowserStack,\nSauceLabs, or TestingBot services.\n\n#### Remote iOS and Android Mobile Devices on the BrowserStack service\n\nFor remotely hosted iOS and Android real devices on the BrowserStack service, refer to the [Browserstack-specific capabilities chart page](https://www.browserstack.com/app-automate/capabilities?tag=w3c)\nfor information regarding the options and capabilities available for the various supported mobile operating systems and\ndevices. BrowserStack uses only real physical devices - simulators are not available on this service.\n\n\n##### Uploading your mobile app(s) to BrowserStack\n\nRefer to the following pages for information on uploading your iOS `.ipa` or Android `.apk` app files to the BrowserStack\nservers:\n- [Upload apps from filesystem](https://www.browserstack.com/docs/app-automate/appium/upload-app-from-filesystem)\n- [Upload apps using public URL](https://www.browserstack.com/docs/app-automate/appium/upload-app-using-public-url)\n- [Define custom ID for app](https://www.browserstack.com/docs/app-automate/appium/upload-app-define-custom-id)\n\nThe preferred method of uploading an app to BrowserStack is to define a custom test ID for your apps to avoid having to\nmodify your test configuration data with a new `app_url` after every app upload. Use the same custom test ID every time\nyou upload a new build of the app.\n\nIf the `UPLOAD_APP` Environment Variable is set to `true` prior to calling the `initialize_appium` method, your iOS `.ipa`\nor Android `.apk` file will automatically be uploaded to the BrowserStack servers prior to running your tests. If you have\nnot specified a custom test ID for your apps, your tests will most likely fail as a new `app_url` will be generated, and\nyou will have to update your test configuration data to use the new `app_url`. If you have specified a custom test ID for\nyour apps, your tests should be able to run immediately after the app file upload has completed.\n\n\n##### BrowserStack Mobile Devices using Environment Variables\n\nIf the `options` hash is not provided when calling the `TestCentricity::AppiumConnect.initialize_appium` method, the following\n**Environment Variables** must be set as described in the table below.\n\n| **Environment Variable** | **Description**                                                                                                                     |\n|--------------------------|-------------------------------------------------------------------------------------------------------------------------------------|\n| `DRIVER`                 | Must be set to `browserstack`                                                                                                       |\n| `BS_USERNAME`            | Must be set to your BrowserStack account user name                                                                                  |\n| `BS_AUTHKEY`             | Must be set to your BrowserStack account access key                                                                                 |\n| `BS_OS`                  | Must be set to `ios` or `android`                                                                                                   |\n| `BS_DEVICE`              | Refer to `deviceName` capability in chart                                                                                           |\n| `BS_OS_VERSION`          | Set to the OS version specified in the `platformVersion` capability in the chart                                                    |\n| `DEVICE_TYPE`            | Must be set to `phone` or `tablet`                                                                                                  |\n| `AUTOMATION_ENGINE`      | Must be set to `XCUITest` for iOS or `UiAutomator2` for Android                                                                     |\n| `APP`                    | Must be set to URL or custom test ID of uploaded iOS `.ipa` or Android `.apk` file                                                  |\n| `ORIENTATION`            | [Optional] Set to `portrait` or `landscape`                                                                                         |\n| `RECORD_VIDEO`           | [Optional] Enable screen video recording during test execution (`true` or `false`)                                                  |\n| `TIME_ZONE`              | [Optional] Specify custom time zone. Refer to `browserstack.timezone` capability in chart                                           |\n| `IP_GEOLOCATION`         | [Optional] Specify IP Geolocation. Refer to [IP Geolocation](https://www.browserstack.com/ip-geolocation) to select a country code. |\n| `SCREENSHOTS`            | [Optional] Generate screenshots for debugging (`true` or `false`)                                                                   |\n| `NETWORK_LOGS`           | [Optional] Capture network logs (`true` or `false`)                                                                                 |\n| `APPIUM_LOGS`            | [Optional] Generate Appium logs (`true` or `false`)                                                                                 |\n| `UPLOAD_APP`             | [Optional] Automatically upload the app to BrowserStack servers if true (`true` or `false`)                                         |\n\nRefer to [**Section 10.4 (Using Configuration Specific Profiles in `cucumber.yml`)**](#using-configuration-specific-profiles-in-cucumber-yml) below.\n\n\n##### BrowserStack Mobile Devices using the `options` Hash\n\nWhen using the `options` hash, the following options and capabilities must be specified:\n- `driver:` must be set to `:browserstack`\n- `device_type:` must be set to `:tablet` or `:phone`\n- `platformName:` must be set to `ios` or `android` in the `capabilities:` hash\n- `'appium:automationName':` must be set to to `XCUITest` for iOS or `UiAutomator2` for Android in the `capabilities:` hash\n- `'appium:platformVersion':` must be set to the version of iOS on the simulator or physical device\n- `'appium:deviceName':` must be set to the name of the iOS simulator or physical device\n- `'appium:app'`: must be set to URL or custom test ID of uploaded iOS `.ipa` or Android `.apk` file\n\n```ruby\n    options = {\n      driver: :browserstack,\n      device_type: phone_or_tablet,\n      capabilities: {\n        platformName: platform,\n        'appium:automationName': automation_name,\n        'appium:platformVersion'","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftestcentricity%2Ftestcentricity_apps","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftestcentricity%2Ftestcentricity_apps","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftestcentricity%2Ftestcentricity_apps/lists"}