Search code examples
macosjavascript-automation

javascript automation click download button in iTunes


I need to programatically download an app from iTunes using JXA. I've done the following:

var its = SystemEvents.processes.byName('iTunes');
delay(3);
its.windows[0].buttons.byName('Get').click();

There is no element selected. I've tried clicking on buttons[0...7] and none of those are the 'Get' button. I assume the button I need is inside a document, biut the JXA documentation clearly states that button elements are children of Window, not of Document. Any ideas on how to click on the corrent button?


Solution

  • A "leaf" UI element such as a button will be at the bottom of a potentially complex hierarchy of UI elements, which is especially true in the case of iTunes.

    To give you a sense, here's an example object specifier for the Get button on a free app's App Store page (assuming you've already ensured that this page is active):

    Application("System Events")
      .applicationProcesses.byName("iTunes")
        .windows.byName("iTunes")
          .splitterGroups.at(0)
            .scrollAreas.at(0)
              .uiElements.at(0)
                .groups.at(3)
                  .buttons.at(0)
    

    The problem is that this object specifier differs across pages, so ideally you'd just apply a filter to all UI elements (via the window's entireContents property) to retrieve the button of interest:

    // Get an array of all UI elements in iTunes window.
    uiElems = Application("System Events").applicationProcesses['iTunes']
               .windows[0].entireContents()
    
    // Find all buttons whose description contains 'Get'.
    btns = uiElems.filter(function(el) { 
      try {
        return el.role() == 'AXButton' 
               &&
               el.description().match(/\bGet\b/)
      } catch (e) {}
    })
    
    // Click on the 1st button found.
    btns[0].click()
    

    Here's the catch: on my reasonably recent machine, this takes about 20 seconds(!).

    I would image that a .whose-style filter would be faster, but I couldn't get it to work in this instance, because exceptions must be caught - as above - but .whose does not seem to support embedded exception handlers.

    If you're willing to make assumptions about a lower level in the hierarchy in whose subtree the button can be found, you can speed things up considerably:

    // Get the group UI elements in one of which the 'Get' button is located.
    grps = Application("System Events").applicationProcesses['iTunes'].
            windows[0].splitterGroups[0].scrollAreas[0].uiElements[0].groups
    
    // Loop over groups
    count = grps.length
    for (i = 0; i < count; ++i) {
      // In the group at hand, find all buttons whose description contains 'Get'.
      btns = grps[i].entireContents().filter(function(el) { 
        try {
          return el.role() == 'AXButton' 
                 &&
                 el.description().match(/\bGet\b/)
        } catch (e) {}
      })
      // Exit loop, if a 'Get' button was found.
      if (btns.length > 0) break  
    }
    
    if (btns.length == 0) {
      console.log('ERROR: No "Get" button found.')
    } else {  
      // Click on the 1st button found.
      btns[0].click()
    }
    

    This runs in less than 1 sec. on my machine.


    UI automation (GUI scripting) is tricky business, unfortunately.

    For interactive exploration, there's the Accessibility Inspector developer tool that comes with Xcode, but using it is non-trivial, especially when it comes to translating findings into code.