Upgrade to Pro — share decks privately, control downloads, hide ads and more …

One Page to test them all!

Priti
April 25, 2015

One Page to test them all!

This is talk Aroj(https://www.linkedin.com/pub/aroj-george/b/573/74b) and I presented at vodQA, Bangalore.

Normally when we have cross platform to support, we end up defining multiple pages for each of the platform irrespective of similar service provided by each. When we came across this situation, we decided to give it chance for having single page which will represent all platforms. This presentation addresses same thing.

You can find more details about the talk and details over here:
https://pritibiyani.github.io/blog/speaking-at-vodqa-banglore

Priti

April 25, 2015
Tweet

More Decks by Priti

Other Decks in Technology

Transcript

  1. A Cross platform mobile automation framework
 Created & Presented by,

    Priti Biyani & Aroj George 
 ONE PAGE TO TEST THEM ALL
  2. U.S. AIRLINE MOBILE APP ๏ iOS, Android, iPad, Blackberry, Windows

    and Mobile Web
 
 ๏ Apple Appstore featured app (Travel)
 2
  3. CALATRAVA Custom open source cross platform javascript framework - Calatrava.

    https://github.com/calatrava/calatrava ‣ Pure native screens ‣ Pure HTML ‣ HTML + Native widgets ‣ Native + HTML Webviews 3
  4. AUTOMATION ๏ Android 
 Calabash Android 
 ๏ iOS 


    Calabash iOS ๏ Mobile Web 
 Watir Webdriver 
 4
  5. PROBLEM STATEMENT 5 
 Create a generic automation framework which

    could support each of the 
 three UI automation tools and work seamlessly across both native and 
 hybrid UI screens
  6. REQUIREMENTS ๏ Should make it easy to add automation. It

    should make life of QA easy!
 ๏ Allow reuse of code and help avoid duplication 
 ๏ Promote higher level business/domain oriented thinking instead of low level UI interactions
 ๏ Make changes in only one place.
 ๏ Once automation is done for a feature on one platform, adding the same for other platforms should be trivial.
 ๏ Keep it simple, just use basic OO concepts. 6
  7. SOLUTION ANALYSIS ๏The Page Object Model Pattern
 ๏Requirements satisfied ✓Domain

    oriented thinking ✓Code reuse/no duplication ✓ Changes in only one place. ✓ Easy to add automation. 7
  8. PAGE OBJECT MODEL SECOND THOUGHTS ๏ ~ 60 screens on

    iOS and Android ๏ ~ 30 screens on Mobile Web ๏ 60 x 2 platforms + 30 = 150 page object classes! ๏ Screens have similar behavior and expose same services ‣Requirements check ✓Allows reuse of code and helps avoid duplication. ✓Changes are required to be done in only one place. ✓Makes it easy to add automation. ✓Promotes higher level business/domain oriented thinking
 ➡ So clearly this approach is not scalable. 8
  9. COMMON PAGE OBJECTS PER SCREEN ๏ page objects per screen

    will solve the class explosion problem. ๏ Requirements satisfied ✓ Should make it easy to add automation. It should make life of QA easy! ✓ Allow reuse of code and help avoid duplication! ✓ Promote higher level business/domain oriented thinking instead of low level UI interactions! ✓ Make change in only one place. ✓ Once automation is done for a feature on one platform, adding the same for other platforms should be trivial. 9
  10. COMMON PAGE OBJECTS - CONCERNS? ๏ Different automation tool APIs

    Element query syntax (xpath/css vs custom calabash syntax) ๏ Different UI actions click/tap on web/mobile ๏ Different platform implementations for same screen locator values can vary ๏ UX interactions patterns for e.g Nav Drawer in Android, the Tab Bar in iOS and the Nav bar in web. 10
  11. COMMON PAGE OBJECTS - MORE CONCERNS ? ๏ No online

    example of this approach.
 ๏ Calabash reference uses a different Page Object class for iOS and Android.
 
 11 ➡ So is it really possible to have common page objects?
  12. OBJECT MODELING Lets try and do a Object Modeling exercise!

    Single page object class ➡ What are the key fields? ➡ What’s the key behavior? 12
  13. PAGE NAME 13 Page Name : String To Refer from

    feature file Login Page Name :“Login”
  14. PAGE IDENTIFIER 14 Page Name : String
 Id : Map

    Identify each page uniquely on the device
 {
 :web => "//div[@id='payment_info']",
 :ios => "all webView css:'#payment_info'",
 :droid => "all webView css:'#payment_info'"
 }
  15. PAGE ID CLASS 15 PageId Id : Map
 exists? ()

    Check if locator specified by id exists on the UI Calabash Android and IOS: 
 not query(locator).empty?
 
 
 Mobile Web (Watir):
 
 Browser.element(:css => locator).present?
  16. ID EXISTS CHECK 16 
 def exists?
 case platform
 when

    ANDROID
 query(ui_query).empty?
 when IOS
 query(ui_query).empty?
 when WEB
 Browser.element(:css => locator).present?
 end end

  17. DRIVER ABSTRACTION 17 Page Name : String
 Id : PageId

    PageId Field : Map
 exists? () 
 PageId Driver Platform : droid
 exists? () 
 Driver Platform : ios exists? () 
 Driver Platform : web
 exists? () 
 def exists?(id_map)
 locator = id_map[:droid]
 begin
 opts.merge!(:screenshot_on_error => false)
 wait_for_elements_exist([locator],opts)
 rescue WaitError
 false
 end
 end 
 def exists?(id_map)
 locator = id_map[:ios]
 begin
 opts.merge!(:screenshot_on_error => false)
 wait_for_elements_exist([locator], opts)
 element_exists locator
 rescue WaitError
 false
 end
 end
 def exists?(id_map, wait=true)
 locator = id_map[:web]
 begin
 Browser.element(:css => locator).wait_until_present if wait
 Browser.element(:css => locator).present?
 rescue TimeoutError
 puts "exists error #{locator}"
 false
 end
 end
  18. ELEMENTS 18 Page Name : String
 Id : PageId
 Elements

    
 Element.new({
 :web => ‘#save_and_continue_button’,
 :ios => "navigationButton marked:'DONE'",
 :droid => "* marked:'Done'"
 } 
 Element Id: Map
 click () 18 def click
 case platform
 when ANDROID
 touch(query(locator))
 when IOS
 touch(query(locator))
 when WEB
 Browser.element(:css => locator).click()
 end end

  19. ELEMENTS DELEGATE TO DRIVER 19 Page Name : String
 Id

    : Map Elements Element Id : Map
 Driver : Driver click() 19 Driver Platform : droid
 exists? 
 click() 
 Driver Platform : ios exists? 
 click() 
 Driver Platform : web
 exists? click() def click(id_map)
 locator = id_map[:droid]
 begin
 scroll_to locator
 rescue RuntimeError
 end
 touch(query(locator))
 wait_for_loader_to_disappear
 end
 def click(id_map)
 locator = id_map[:ios]
 begin
 scroll_to locator
 rescue RuntimeError
 end
 touch(query(locator))
 wait_for_loader_to_disappear
 end
 def click(id_map)
 locator = id_map[:web] B.element(:css => locator).wait_until_present
 B.element(:css => locator).click
 wait_for_loader_to_disappear
 end

  20. MORE ELEMENTS 20 Page Name : String
 Id : PageId


    Element 1: Element Element 2: Element …. Element 20 Driver Platform : droid
 exists? 
 click() 
 setText()
 getText()
 checked()
 
 Textbox setText()
 getText() id : map Checkbox checked?()
 check(item) id : map Dropdown select(item)
 id : map Id : Map
 Driver : Driver Driver Platform : ios
 exists? 
 click() 
 setText()
 getText()
 checked()
 
 Driver Platform : web
 exists? 
 click() 
 setText()
 getText()
 checked()
 
 exists? ()

  21. PAGE TRANSITION 21 21 Login Home Page def login
 click_login_button


    wait_for_home_page_to_load
 return HomePage.new
 end [ success ] 
 def <action>
 click_button
 wait_for_<next_page>_to_load
 return <NextPage.new>
 end
  22. TRANSITION MODELING 1. driver.click ✓ Driver should be responsible only

    for UI interaction. ✓ Driver should not know about higher level abstraction Page.
 2. element.click ✓ It will not be applicable to all elements. 
 3. Transition Aware Element ➡ An element that understands page transitions. 22
  23. TRANSITION ELEMENT 23 Page Name : String
 Id : PageId


    Elements Element 23 Driver Platform : droid
 exists? 
 click() 
 setText()
 getText()
 checked()
 
 Textbox setText()
 getText() id : map Checkbox checked?()
 check(item) id : map Dropdown select(item)
 id : map Id : Map
 Driver : Driver Driver Platform : ios
 exists? 
 click() 
 setText()
 getText()
 checked()
 
 Driver Platform : web
 exists? 
 click() 
 setText()
 getText()
 checked()
 
 exists? ()
 TransitionElement click(item)
 id : map
  24. TRANSITION ELEMENT 24 @login_button = TransitionElement.new(
 { 
 :web =>

    '#continue_button',
 :ios => "* marked:'Login'",
 :droid => "* marked:'Login'"
 },
 {
 :to => HomePage,
 }
 )
 
 Next Page Class
  25. TRANSITION ELEMENT - MULTIPLE TRANSITION 25 25 Login Admin Home

    Page Member Home Page userType? Adm in M em ber [success]
  26. TRANSITION ELEMENT - MULTIPLE TRANSITION @login_button = TransitionElement.new(
 { 


    :web => '#continue_button',
 :ios => "* marked:'Login'",
 :droid => "* marked:'Login'"
 },
 {
 :to => [AdminPage, UserPage],
 
 }
 )
 Multiple Transition
  27. TRANSITION ELEMENT - ERROR TRANSITION Login Admin Home Page Member

    Home Page userType? Adm in [ SUCCESS ] [ FAIL ] M em ber
  28. TRANSITION ELEMENT - ERROR TRANSITION @login_button = TransitionElement.new(
 { 


    :web => '#continue_button',
 :ios => "* marked:'Login'",
 :droid => "* marked:'Login'"
 },
 {
 :to => [AdminPage, UserPage],
 :error => Login
 }
 )
 
 Error Representation
  29. MULTIPLE TRANSITIONS - WRONG TRANSITION ‣ Scenario: If there is

    a bug in code, app transitions to wrong page from list of multiple transitions. ‣ Should transition element detect this bug?
 ✓ but this is application logic ✓ will increase complexity ➡ Tests should take care for assertions of correct page, not the framework.
 

  30. TRANSITION ELEMENT 30 def wait_till_next_page_loads(next_pages, error_page)
 
 has_error, found_next_page =

    false, false
 
 begin
 wait_for_element(timeout: 30) do
 found_next_page = next_pages.any? { |page| page.current_page?}
 has_error = error_page.has_error? if error_page
 found_next_page or has_error
 end
 has_error ? error_page : next_pages.find { |page| page.current_page?}
 rescue WaitTimeoutError
 raise WaitTimeoutError, "None of the next page transitions were found. Checked for: => #{next_page_transitions.join(' ,')}"
 end
 end
 
 

  31. PAGE OBJECT EXAMPLE - SEAT MAP 31 Native Implementation Common

    code across platform Iphone Mobile Web Android
  32. SEAT MAP FEATURE STEP 32 And I select following seat


    | origin | destination | flight_number | pax_name | seat_number |
 | ORD | JFK | DL3723 | Rami Ron | 9C | | ORD | JFK | DL3723 | John Black | 10C |
 | JFK | ATL | DL3725 | Rami Ron | 12C | | JFK | ATL | DL3725 | John Black | 13C |
 

  33. SEATMAP PAGE 33 SeatMap Name :“Seat map” Id : {…

    }
 select_seat(pax, flight, seat_number)
  34. SEAT MAP FEATURE STEP 34 And(/^I select following seat$/) do

    |table|
 
 table.hashes.each do |row|
 flight = Flight.new(row['origin'], row['destination'], row[‘flight_number’]) seat_map_page = SeatMap.new
 seat_map_page.select_seat row['pax_name'], flight, row['seat_number']
 end end And I select following seat
 | origin | destination | flight_number | pax_name | seat_number |
 | ORD | JFK | DL3723 | Rami Ron | 9C | | ORD | JFK | DL3723 | John Black | 10C |
 | JFK | ATL | DL3725 | Rami Ron | 12C | | JFK | ATL | DL3725 | John Black | 13C |
 

  35. PAGE OBJECT EXAMPLE 35 class SeatMap < Page
 def initialize


    @id = PageId.new({ :web => "//div[@id='ism']//div[@id='sb_seat_map_container']",
 :ios => "webView css:'#seat_map_container'",
 :droid => "webView css:'#seat_map_container'"
 }) 
 @leg = Field.element({ 
 :web => "label[@class='dd-option-text'][text()='%s: %s to %s']",
 :ios => "label marked:'%s: %s to %s'",
 :droid => "* marked:'%s: %s to %s'"
 }) @passenger = Field.element({
 :web => "//label[@class='dd-option-text'][text()='%s']",
 :ios => "label marked:'%s'",
 :droid => "* marked:'%s'"
 }) @passenger_seat_number_text = Field.element({
 :web => "//label[@class='dd-selected-sec-text'][text()='%s']",
 :ios => "label {text CONTAINS '%s'}",
 :droid => "* marked:'%s'"
 }) @seat_button = Field.element({
 :web => “//div[@id='seat_%s']", :ios => "webView css:'#seat_%s'",
 :droid => "webView css:'#seat_%s'"
 })
  36. PAGE OBJECT EXAMPLE 36 class SeatMap < Page def select_seat_for_pax_for_leg(flight_leg,

    pax_name, seat_number)
 select_leg flight_leg
 select_passenger pax_name
 select_seat seat_number
 end
 def select_leg(flight_leg)
 @leg_header.click
 @leg.click(flight_leg.flight_number, flight_leg.origin, flight_leg.destination)
 end
 def select_passenger(pax_name)
 @passenger_name_header.click
 @passenger.click(pax_name)
 end
 def select_seat(seat_no)
 @seat_button.click(seat_no)
 @passenger_seat_number_text.await(seat_no)
 end 
 "label marked:'%s: %s to %s'" 
 "label marked:'%s'" 
 "webView css:'#seat_%s'"
  37. TRANSITION TO NEXT PAGE 37 class SeatMap < Page
 def

    initialize
 @done_button = Field.transition_element({
 :web => "#save_and_continue_button",
 :ios => "navigationButton marked:'DONE'",
 :droid => "* marked:'Done'"
 },
 {
 :to => [SeatsDetails],
 })
 
 super(‘Seat Map')
 end
 
 def close
 @done_button.click
 end end
  38. TRANSITION TO SEATS DETAILS PAGE 38 def verify_seat_details seat_details_page, table

    
 table.hashes.each do |row|
 flight = Flight.new(row['origin'], row['destination'], row['flight_number'])
 
 actual_seat_number = seat_details_page.get_seat_number(row[‘pax_name'], flight)
 
 expected_seat_number = row['seat_number']
 expect(actual_seat_number).to eq(expected_seat_number)
 end
 end And(/^I select following seat$/) do |table|
 
 table.hashes.each do |row|
 flight = Flight.new(row['origin'], row['destination'], row[‘flight_number’]) seat_map_page = SeatMap.new
 seat_map_page.select_seat row['pax_name'], flight, row['seat_number']
 end seat_details_page = seat_map_page.close verify_seat_details seat_details_page, table end
  39. COMMON PAGE OBJECTS - SOLUTION ๏ Different automation tool APIs/

    Different UI actions ➡ Driver abstraction
 ๏ Different platform implementations for same screen ➡ Element locator map.
 ๏ UX interactions patterns (Nav Drawer / Tab bar /Menu) ➡ How? 39
  40. MENU/MENU ITEM 41 Menu Name : String
 Id : Map

    MenuItems MenuItem Name : String
 TargetPage : Page Type : MenuType MenuButton:TransitionElement show()
 show_secondary_menu()
 launch (item_name)
 41 def launch(item_name) show
 menu_item = @menu_items[item_name]
 show_secondary if menu_item.is_secondary?
 menu_item.click
 end
 
 click ()
 is_secondary? ()
 

  41. MENU IMPLEMENTATION 42 class Menu
 
 SHOP_AND_BOOK = 'Book'
 MY_TRIPS

    = 'My Trips'
 FLIGHT_STATUS = 'Flight Status’ 
 def initialize()
 @id = PageId.new({
 :web => "//div[@id='home']//ul[@id='home_options']",
 :ios => "tabBarButtonLabel marked:'Book'",
 :droid => "* id:'drawer_items_list'"
 })
 
 @primary_menu_button = Field.element ({ web: ‘#menu', :ios => '', :droid => "* id:'home'" }),
 @secondary_menu_button = Field.element ({ web: '', :droid => '', :ios => "* marked:'More'" }),
 @menu_items = {
 SHOP_AND_BOOK => MenuItem.new(SHOP_AND_BOOK, FlightSearch),
 MY_TRIPS => MenuItem.new(MY_TRIPS, MyTrips),
 FLIGHT_STATUS => MenuItem.new(FLIGHT_STATUS, FlightStatus, ios: MenuItem::SECONDARY)}
 
 end
 def show
 @primary_menu_button.click
 end
 
 def show_secondary
 @secondary_menu_button.click
 end
 
 def launch(item_name) . . . end 
 end
 
 

  42. MENU ITEM IMPLEMENTATION 43 class MenuItem
 PRIMARY = 1
 SECONDARY

    = 2
 attr_accessor :name, :page, :type
 
 def initialize(name, page, type = PRIMARY)
 @name = name
 @page = page
 @type = type
 @menu_button = Field.transition_element(
 {
 :web => "//li[@class='#{@name.downcase}']",
 :ios => "label marked:'#{@name}'",
 :droid => "WhitneyDefaultTextView id:'drawer_item_text' text:'#{@name}'"
 },
 {
 :to => @page
 })
 end
 
 def click
 @menu_button.click
 end
 
 def is_secondary?
 @type[Driver.platform] == SECONDARY
 end
 end
 
 MenuItem.new(FLIGHT_STATUS, FlightStatus, ios: MenuItem::SECONDARY)
  43. COMMON PAGE OBJECTS - SOLUTION ✓ Different automation tool APIs

    ➡ Driver abstraction
 ✓ Different UI actions ➡ Driver abstraction
 ✓ Different platform implementations for same screen ➡ Element locator map.
 ✓ UX interactions patterns (Nav Drawer / Tab bar /Menu) ➡ Menu and Menu Item Abstraction 44
  44. IMPLEMENTATION NOTES ‣ We "require" specific driver class during test

    execution. 
 ‣ Page registry, can be queried for page object instances. ‣ PageRegistry.get "Login Page”
 ‣ Rspec Unit Tests ‣ Rake task to create new page classes. 45
  45. FUTURE DIRECTION ‣ Debug Mode
 ‣ Log debug info like

    element id, clicks etc…
 ‣ Slow element locator finder
 46
  46. SUMMARY ๏ Good OO design is the key 
 ๏

    Good abstractions can help solve hard problems 48
  47. CONCLUSION 49 
 Today, to add automation to support new

    feature development across the three different platforms requires changes in just one place.
  48. Reach out to us at Aroj George @arojp Priti Biyani

    @pritibiyani
 
 
 http://arojgeorge.ghost.io/
 http://pritibiyani.github.io/
 THANK YOU