Applescript to click on search results/other web page buttons and links? - applescript

How would I use Applescript to click on a web link in a google search. Can I identify them by name or number or anything?

Safari:
tell application "Safari"
open location "http://google.com/search?q=example"
do JavaScript "window.addEventListener('load', function() {
document.querySelectorAll('.r a')[0].click()
})" in document 1
end tell
Chrome:
tell application "Google Chrome"
open location "http://google.com/search?q=example"
tell active tab of window 1
execute javascript "window.addEventListener('load', function() {
document.querySelectorAll('.r a')[0].click()
})"
end tell
end tell
Edit: try something like this to match a YouTube result by title:
document.querySelector('.yt-uix-tile-link').click()
Edit 2: changed window.onload=function(){} to window.addEventListener('load',function(){}).

Building on #Lauri Ranta's great answer, here is convenience function clickOn(), which:
accepts a target document, a CSS selector string plus a zero-based index to select among what the selector string matches, and simulates a click on the element thus identified.
works irrespective of whether the target document is still being loaded or has already fully loaded - this turned out to be non-trivial. (Lauri's code relies on the window object's load event not to have fired yet).
works with both Safari and Google Chrome (if you don't have Chrome installed, you'll have to comment out a few lines)
Examples that use a delay to demonstrate that the click works even after the document has fully loaded:
# SAFARI
# Click on the *2nd* result returned from googling 'example' with Safari:
tell application "Safari"
open location "http://google.com/search?q=example"
delay 5 # sample delay - NOT needed
my clickOn(document 1, ".r a", 1)
end tell
# CHROME
tell application "Google Chrome"
open location "http://google.com/search?q=example"
delay 5 # sample delay - NOT needed
my clickOn(active tab of window 1, ".r a", 1)
end tell
clickOn source code:
on clickOn(doc, cssSelector, ndx)
# If no explicit index (into the matches returned by the CSS selector)
# is specified, default to 0.
if ndx is missing value or ndx = "" then set ndx to 0
# Synthesize the JavaScript command.
set jsCode to "
(function () { // Use a closure to avoid polluting the global namespace.
function doIt(t) { // Helper function
if (doIt.done) return; // Already successfully called? Ignore.
try {
// Perform the desired action - may fail if invoked too early.
document.querySelectorAll('" & cssSelector & "')[" & ndx & "].click();
} catch(e) {
return; // Return without setting the success 'flag'.
}
doIt.done=true; // Set success 'flag' as a property on this function object.
};
// Attach a listener to the window's load event for invoking the helper function then.
window.addEventListener('load', doIt);
// If the document signals readiness -- which may still be too early, we can't know --
// also try to invoke the helper function *directly*.
if (document.readyState === 'complete') { doIt(); }
})();
"
# Execute the JavaScript command in the target page.
if class of doc as text is "document" then # Safari: a «document» instance was passed
using terms from application "Safari"
tell doc to do JavaScript jsCode
end using terms from
else # Google Chrome: a «tab» instance was passed
# !! IF CHROME IS NOT INSTALLED, SIMPLY DEACTIVATE THIS `using terms from` BLOCK.
using terms from application "Google Chrome"
tell doc to execute javascript jsCode
end using terms from
end if
end clickOn

Related

Getting values from inner functions of current webpage in Chrome via (JXA / AppleScript)

I can't get values from inner functions of current webpage in Chrome. Why it's happened?
for examples in JXA:
function getTime() {
const chrome = Application('Google Chrome')
const currentTab = chrome.windows[0].activeTab()
return currentTab.execute({javascript: 'jwplayer().getPosition()'})
}
getTime()
Chrome returns reference error:
Uncaught ReferenceError: jwplayer is not defined
in AppleScript the same:
tell application "Google Chrome"
return execute front window's active tab javascript "jwplayer().getPosition()"
end tell
Settings in Chrome is checked: "Allow Javascript from Apple Events"
Founded a workaround with AppleScript:
tell front window of application "Google Chrome"
set URL of active tab to "javascript:
sessionStorage.setItem('scrapy', JSON.stringify({
currentTime: jwplayer().getPosition()
}, 2, ' ')
"
return {execute active tab javascript "sessionStorage.getItem('scrapy')"}
end tell

Applescript: Truncated feed URL

I'm running into errors with this simple script. It's a validator to validate RSS feed with multiple validation sites using Safari. Everything works fine as long as the feed does not contain special characters or anything after the = sign.
The script should validate the feed that was copied to the clipboard.
For example, this feed works fine: http://thefirst.libsyn.com/rss
This feed gets truncated after ?id: https://www.npr.org/rss/podcast.php?id=510298
This is only happening on the Podbase validator site.
If I could get the script to click the Validate and Go buttons, that would be amazing, but this is pretty basic…just stuck as to why the feed is getting truncated.
set feed_url to the clipboard as string
set the podbaseurl to "http://podba.se/validate/?url=" & feed_url
set the feedvalidatorurl to "http://feedvalidator.org/check.cgi?url=" & feed_url
set the castfeedurl to "http://castfeedvalidator.com/?url=" & feed_url
tell application "Safari"
make new document
open location podbaseurl
open location feedvalidatorurl
open location castfeedurl
end tell
The problem is that https://podba.se/validate only makes it look like a single GET request is enough, whereas clicking the Go button interactively performs many individual GET requests behind the scenes whose results are pieced together on the current page (and the URL is then modified to include the submitted feed URL).
In other words: even solving the (odd) truncation problem wouldn't be enough.
Therefore, your best bet is indeed to simulate an interactive submission of a feed URL, which requires filling an input box with the feed URL and pressing the submission button.
Interactive submission must be simulated for the http://castfeedvalidator.com site as well, where pressing the submission button is sufficient, however.
(As you report, even though inspecting the request sent by the submission button shows that a variation of your URL - which only prepares the feed URL for submission - can be used to instantly submit it, doing so doesn't render the results correctly (missing styles)).
The following code implements both suggestions (the simulated-interaction approach was adapted from this answer of mine):
# Sample value
set the clipboard to "https://www.npr.org/rss/podcast.php?id=510298"
set feed_url to the clipboard as string
# Base URL only; the feed URL will be submitted by simulated interaction below.
set the podbaseurl to "http://podba.se/validate/"
set the feedvalidatorurl to "http://feedvalidator.org/check.cgi?url=" & feed_url
set the castfeedurl to "http://castfeedvalidator.com/?url=" & feed_url
tell application "Safari"
activate
set newDoc to make new document
set newWin to front window
# Simulate interactive submission of feed_url at podbaseurl.
set URL of newDoc to podbaseurl
my submitUrl(newDoc, podbaseurl, feed_url)
# The feedvalidateorurl can be opened normally.
open location feedvalidatorurl
# Simulate interactive submission of feed_url at castfeedurl.
set newTab to make new tab in newWin
set URL of newTab to castfeedurl
my submitUrl(newTab, castfeedurl, feed_url)
end tell
on submitUrl(doc, target_url, feed_url)
# Synthesize the JavaScript command.
set jsCode to "
(function () { // Use a closure to avoid polluting the global namespace.
function doIt(t) { // Helper function
if (doIt.done) return; // Already successfully called? Ignore.
try {
// Perform the desired action - may fail if invoked too early.
if (/^http:\\/\\/podba.se/.test('" & target_url & "')) {
document.querySelector('#url-input').value = '" & feed_url & "';
document.querySelector('#url-input').dispatchEvent(new Event('input'));
setTimeout(function() { document.querySelector('#go-button').click(); }, 0);
} else { // http://feedvalidator.org
document.querySelector('.btn-subscribe').click()
}
} catch(e) {
return; // Return without setting the success 'flag'.
}
doIt.done=true; // Set success 'flag' as a property on this function object.
};
// Attach a listener to the window's load event for invoking the helper function then.
window.addEventListener('load', doIt);
// If the document signals readiness -- which may still be too early, we can't know --
// also try to invoke the helper function *directly*.
if (document.readyState === 'complete') { doIt(); }
})();
"
# Execute the JavaScript command in the target page.
tell application "Safari"
tell doc to do JavaScript jsCode
end tell
end submitUrl

Is there a way to clear/refresh the accessibility hierarchy cache

I have a UI test that checks the value of static text element, waits a few seconds and checks again to confirm a change. At first it wasn't working because the hierarchy was not updating. I noticed this in the log;
Use cached accessibility hierarchy for
I've put in a workaround for this by simply adding a tap to a menu and opening/closing it so that an event is synthesized and the hierarchy is updated.
It would be better, however, if there was a way to clear the cache directly or force and update. I haven't found one in the API. Am I missing something?
Any ideas?
this is what I am doing;
XCTAssertEqual(app.staticTexts["myText"].label, "Expected 1")
sleep(20)
menu.tap()
sleep(1)
menu.tap()
XCTAssertEqual(app.staticTexts["myText"].label, "Expected 2")
What I'd like to be able to do it
XCTAssertEqual(app.staticTexts["myText"].label, "Expected 1")
sleep(20)
app.elements.refresh()
XCTAssertEqual(app.staticTexts["myText"].label, "Expected 2")
In order to force an update of the accessibility hierarchy, request the count property for any XCUIElementQuery:
// refresh
_ = XCUIApplication().navigationBars.count
// examine
print(XCUIApplication().debugDescription)
The above results in: "Get number of matches for: Descendants matching type NavigationBar" and "Snapshot accessibility hierarchy for com.myapp".
The following works for me in Xcode 10.2 (10E125):
import XCTest
extension XCUIApplication {
// WORKAROUND:
// Force XCTest to update its accessibility cache. When accessibility data
// like NSObject.accessibility{Label|Identifier} changes, it takes a while
// for XCTest to catch up. Calling this method causes XCTest to update its
// accessibility cache immediately.
func updateAccessibilityCache() {
_ = try? snapshot()
}
}
You should use expectationForPredicate, along the lines of...
let myText = app.staticTexts["myText"]
let waitFor = NSPredicate(format: "label = 'Expected 2'")
label.tap()
self.expectationForPredicate(waitFor, evaluatedWithObject: myText, handler: nil)
self.waitForExpectationsWithTimeout(2.0, handler: nil)
This will wait until either myText's label is 'Expected 2', or the timeout of 2 seconds is reached.
In my case, it is a problem because I'm trying to test for Facebook login, which uses Safari controller. It looks like Facebook has updated the UI after cache.
So you need to wait a bit, use the wait function here https://stackoverflow.com/a/42222302/1418457
wait(for: 2)
let _ = app.staticTexts.count
But the above is just workaround and very flaky. A more correct approach would be to wait for a certain element to appear, see https://stackoverflow.com/a/44279203/1418457

How to click UI element using JXA

Please tell me how do I click in point coordinates in application window?
I trying to UI automate my application on OSX 10.10 using JXA technology.
In documentation I found that it's possible using click at event. By I'am beginner of JXA and cant find how make a call.
Code snippet which I tried in Script Editor:
var app = Application('my_application_path')
app.window.click.at('{100,100}')
Thank you for help
You can interact with an application's user interface using the System Events application. Here is a script that clicks at certain coordinates in Safari:
// Activate Safari, so you will be able to click like a user
Application("Safari").activate()
// Access the Safari process of System Events
var SystemEvents = Application("System Events")
var Safari = SystemEvents.processes["Safari"]
// Call the click command, sending an array of coordinates [x, y]
Safari.click({ at: [300, 100] })
If you want to click a specific button (or other element of the user interface), it is more appropriate to click that specific element. For example:
// Click the third button of Safari's first window to minimize it
Safari.windows[0].buttons[2].click()
To learn what user interface elements can be interacted with and how, check out the Processes Suite in System Events' scripting dictionary. To open the dictionary, in Script Editor's menu bar, choose Window > Library, then select System Events in the Library window.
See https://github.com/dtinth/JXA-Cookbook/wiki/System-Events#clicking-menu-items
For example:
var fileMenu = proc.menuBars[0].menuBarItems.byName('File');
Below is an example of a portion of a script I wrote that automates creating mailboxes (aka folders) in Mail. I ended up using the UI file menus and click because using make() in the Mail DOM had issues for me. Hope it helps someone.
(() => {}
//this is part of a script that automates creating mailboxes (ie folders) in Apple Mail
//I used the file menu UI because when I tried the Mail library and make() method
//there was strange behavior when trying to interact with the new mailbox.
//However, when creating the new mailboxes thru the file menu, all seems to work fine
const Mail = Application('Mail');
const strId = Mail.accounts.byName('Exchange').id();
const exchange = Mail.accounts.byId(strId);
const activeClientFolder = exchange.mailboxes.byName('ActiveClient');
const SysEvents = Application('System Events');
const mail = SysEvents.processes.byName('Mail');
//next two lines insure Mail will be open and in front
mail.windows[0].actions.byName('AXRaise').perform();
mail.frontmost = true;
const mailboxMenu = mail.menuBars[0].menus.byName('Mailbox');
//below shows EXAMPLES of using .click(), keystroke(), and keyCode()
let newFolder = function (parentFolder, newFolderName, addTrailingDelay = true) {
//next line will select the parent mailbox (aka folder) where the new mailbox will be inserted
Mail.messageViewers[0].selectedMailboxes = parentFolder;
mailboxMenu.click();
delay(.2);
mailboxMenu.menuItems.byName('New Mailbox…').click();
delay(.2);
SysEvents.keystroke(newFolderName);
SysEvents.keyCode(36);
//delay is needed when creating multiple mailboxes with a loop
if (addTrailingDelay == true){
delay(1);
}
}
//now the payoff
const count = newActiveClients.length;
for(let i=0;i<count;i++){
/* Client Root Mailbox */
newFolder(activeClientFolder, newActiveClients[i], true);
/* Client Email Folders */
newFolder(activeClientFolder.mailboxes.byName(newActiveClients[i]), 'Client', true);
newFolder(activeClientFolder.mailboxes.byName(newActiveClients[i]).mailboxes.byName('Client'), 'Client_FYI_Sent');
newFolder(activeClientFolder.mailboxes.byName(newActiveClients[i]).mailboxes.byName('Client'), 'Client_FYI_Inbox');
newFolder(activeClientFolder.mailboxes.byName(newActiveClients[i]).mailboxes.byName('Client'), 'Client_FYI_Client_To');
newFolder(activeClientFolder.mailboxes.byName(newActiveClients[i]).mailboxes.byName('Client'), 'Client_From', false);
}
})()

How do you automatically confirm a modal in ruby using page-object?

I have a standard rails application with a delete link. This delete link comes up with a browser popup modal (using rails confirm option).
I am currently attempting to test the delete function with Cucumber, Selenium-Webdriver (or Watir-Webdriver, still haven't decided), and the page-object gem.
Once the modal has been triggered, anything I do on the page gives me the following error:
Modal dialog present (Selenium::WebDriver::Error::UnhandledAlertError)
I have been looking all over, but cannot find a way to handle this condition. If possible, I would like to continue using the PageFactory module in the page-object gem.
How can I dismiss/accept the modal?
I figured out a way to do this, but haven't decided upon the exact implementation.
In Javascript you can overwrite any function, which means you can overwrite the confirm
This means that you can run the following code to disable any popups.
def disable_popups
# don't return anything for alert
browser.execute_script("window.alert = function() {}")
# return some string for prompt to simulate user entering it
browser.execute_script("window.prompt = function() {return 'my name'}")
# return null for prompt to simulate clicking Cancel
browser.execute_script("window.prompt = function() {return null}")
# return true for confirm to simulate clicking OK
browser.execute_script("window.confirm = function() {return true}")
# return false for confirm to simulate clicking Cancel
browser.execute_script("window.confirm = function() {return false}")
end
If you put this inside the initalize_page function of a page-object then the dialogs are automatically removed.
def initialize_page
disable_popups
end
Or you could do it right before the pop is triggered
def delete
disable_popups
delete_link # => clicks the link
end
References:
Testing Webpages with Javascript Popups Correctly
Dismissing Pesky Javascript Dialogs with Watir
The page object gem has methods to handle javascript popups - see the original page-object gem post. In your case, I believe you want the confirm method::
(String) confirm(response, frame = nil, &block)
Override the normal confirm popup so it does not occurr.
Examples:
message = #popup.confirm(true) do
#page.button_that_causes_confirm
end
Parameters:
what (bool) — response you want to return back from the confirm popup
frame (defaults to: nil) — optional parameter used when the confirm is nested within a frame
block — a block that has the call that will cause the confirm to display
Returns:
(String) — the message that was prompted in the confirm
There is similar for the alert and prompt popups.
In your page-object, I would define:
class MyPage
link(:delete_link, :id=>'delete')
def delete()
confirm(true){ delete_link }
end
end
Then when you call page.delete, it will click the link and confirm the popup.

Resources