Click on a button of a web site with JavaScript

I’m trying to learn a little about clicking on buttons with JavaScript. My test scripts are from here. A MacScripter thread is here. My goal is to click on the “Top” button on the main MacScripter page here. The Inspect data for the “Top” button from Google Chrome is:

My test scripts, none of which work, are as follows:

--test web page is https://www.macscripter.net

--click by ID
clickID("ember136") --doesn't work and ID changes
to clickID(theId)
	say "ID"
	tell application "Safari"
		activate
		delay 1
		do JavaScript "document.getElementById('" & theId & "').click();" in document 1
	end tell -- tells Applescript you are done talking to Safari
end clickID -- lets AppleScript know we are done with the function

--click by name
# clickName("button", 0) --name not available
to clickName(theName, elementnum)
	say "name"
	tell application "Safari"
		activate
		delay 1
		do JavaScript "document.getElementsByName('" & theName & "')[" & elementnum & "].click();" in document 1
	end tell
end clickName

--click by class name
# clickClassName("nav-item_top top ember-view", 0) --doesn't work
to clickClassName(theClassName, elementnum)
	say "Class"
	tell application "Safari"
		activate
		delay 1
		do JavaScript "document.getElementsByClassName('" & theClassName & "')[" & elementnum & "].click();" in document 1
	end tell
end clickClassName

--click by tag name
# clicktagName("Top", 0) --doesn't work
to clicktagName(thetagName, elementnum)
	say "Tag"
	tell application "Safari"
		activate
		delay 1
		do JavaScript "document.getElementsByTagName('" & thetagName & "')[" & elementnum & "].click();" in document 1
	end tell
end clicktagName

How can I get these scripts to work? I have enabled the Allow JavaScript from Apple Events setting in Safari.

I’ve had a few successes but only with class name:

--https://www.google.com
--Sign in button
clickClassName("gb_Ta", 0)
to clickClassName(theClassName, elementnum)
	tell application "Safari"
		do JavaScript "document.getElementsByClassName('" & theClassName & "')[" & elementnum & "].click();" in document 1
	end tell
end clickClassName

--news.google.com
--News Showcase item
--clickClassName("EctEBd", 3)
--to clickClassName(theClassName, elementnum)
--	tell application "Safari"
--		do JavaScript "document.getElementsByClassName('" & theClassName & "')[" & elementnum & "].click();" in document 1
--	end tell
--end clickClassName

--https://www.macscripter.net/c/scripting-forums/5
--Log in button
--clickClassName("d-button-label", 0)
--to clickClassName(theClassName, elementnum)
--	tell application "Safari"
--		do JavaScript "document.getElementsByClassName('" & theClassName & "')[" & elementnum & "].click();" in document 1
--	end tell
--end clickClassName

This same process seems to work with Google Chrome:

--click on "Log In" button
--https://www.macscripter.net/

clickClassName("d-button-label", 0)

to clickClassName(theClassName, elementnum)
	tell application "Google Chrome"
		execute tab 1 of window 1 javascript "document.getElementsByClassName('" & theClassName & "')[" & elementnum & "].click();"
	end tell
end clickClassName

It also works in a shortcut, although, if the button is not found, the shortcut shows the following incorrect error message:

Unable to Run JavaScript on Web Page. Make sure that “Allow JavaScript from Apple Events” is enabled in the Develop menu in Safari. The Develop menu can be enabled in the Advanced section of Safari’s Preferences.

I thought I would add a few tentative thoughts on this topic. First, before doing anything else, you have to enable the Safari option to Allow JavaScript from Apple Events.

Second, the only script approach I have been able to use is the one that uses class name, and the general process I follow is:

  • View the applicable site data in Safari by right-clicking on the button and selecting Inspect Element.
  • Look for a class name in the displayed data in the highlighted and surrounding areas.
  • Select a class name for testing up to the first space. Selecting the entire class name doesn’t seem to work for me.
  • Insert the class name in the script and sequentially test difference element numbers beginning with “0”.
  • Have the web site active behind the script editor, so that you’ll know when you’ve found the correct combination.

The above process involves a lot of trail and error and doesn’t work in all or even most circumstances. Also, I wonder how reliable a working script will be over time. However, it’s an approach to consider when others fail.

I tested this approach on the slick deals shopping site, and I was able to get a working script in perhaps 4 minutes.

--https://slickdeals.net/forums/forumdisplay.php?f=9

set className to "slickdealsHeader__navItemWrapper"
set elementNumber to "0" -- 1, 2, and 3 work for other buttons (e.g. "Post a Deal")

tell application "Safari" to do JavaScript "document.getElementsByClassName('" & className & "')[" & elementNumber & "].click();" in document 1

This shortcut is functionally equivalent to the above script:

Safari Button Click.shortcut (22.0 KB)

I decided to write a working script for additional learning on this topic. The following script logs me into MacScripter and works in my testing. However, I’d like to replace the GUI scripting with JavaScript, and I wondered if anyone could help me with this? BTW, the second JavaScript code line clicks on a button by ID, and it was good to actually get this to work in a script. Thanks for the help.

--test site is https://www.macscripter.net
--edit needed to eliminate and reduce delays

set userName to "peavine"
set the clipboard to userName
set userPassword to "xxx" --not my real password

tell application "Safari"
	activate
	tell document 1 to do JavaScript "document.getElementsByClassName('d-button-label')[1].click();" --Log In button
end tell
delay 0.5

tell application "System Events"
	keystroke "v" using {command down} --paste user name
	delay 0.5
	set the clipboard to userPassword
	delay 0.5
	key code 48 --tab to password text box
	delay 0.5
	keystroke "v" using {command down} --paste user password
end tell
delay 0.5

tell application "Safari" to tell document 1 to do JavaScript "document.getElementById('login-button').click();" --Log in button

Shouldn’t you take advantage of Safari’s autofill? That would be more secure.

Mockman. I agree with what you say. I’m just using the log-in process for testing purposes to learn JavaScript.

I found the commands I needed to replace the GUI scripting. The only issue is that the forum software does not correctly convert the password string into a hidden password, causing the log-in to fail. So, I used GUI scripting for this one task. Timing appears to be an issue when using Javascript to accomplish tasks such as this and at least some of the delays in the following script are necessary.

--test site is https://www.macscripter.net

set userName to "peavine"
set userPassword to "xxx" --not my actual password
set delayLength to 0.5 --test different values
set the clipboard to userPassword

tell application "Safari"
	activate
	delay  delayLength
	tell document 1
		do JavaScript "document.getElementsByClassName('d-button-label')[1].click();" --click log in button
		delay delayLength
		do JavaScript "document.getElementById('login-account-name').value = '';" --empty user name
		do JavaScript "document.getElementById('login-account-name').value = 'peavine';" --enter user name
		delay delayLength
		do JavaScript "document.getElementById('login-account-password').value = '';" --empty password
		do JavaScript "document.getElementById('login-account-password').focus();" --move focus to password field
		delay delayLength
		pasteClipboard() of me --paste password
		delay delayLength
		do JavaScript "document.getElementById('login-button').click();" --click log in button
	end tell
end tell

on pasteClipboard()
	tell application "System Events" to keystroke "v" using {command down}
end pasteClipboard
1 Like

Here is the JavaScript you need to click the “Top” link on the MacScripter landing page.

document.getElementsByTagName(‘a’)[12].click()

As the name of the function implies the ‘getElementsByTagName’ will return an HTML collection containing all of the anchors or links in the full page. You then have to find the one you want, in this case it is the 13th one. As with most collections in JavaScript they are zero based so the index will be 12.

The best way to work with JavaScript in a browser is to use the developer tools. All major modern browsers have these developer tools but they may differ on where they are located or whether they are made available without setting a preference. It is best to look in the Help or documentation for the browsers. I just played with the console and inspector tools in my browser, in this case it was Firefox, to determine the exact command.

1 Like

Pseudotsuga. Thanks for the suggestion. I didn’t have a working example of the getElementsByTagName function and it’s good to have one.

BTW, what does the tag name ‘a’ stand for. I did a Google search and couldn’t find anything.

The ‘a’ apparently stands for ‘anchor’.

https://www.w3.org/TR/1999/REC-html401-19991224/struct/links.html#edef-A

<!ELEMENT A - - (%inline;)* -(A) -- anchor -->

Also from the v4 standard page:

12.1 Introduction to links and anchors

HTML offers many of the conventional publishing idioms for rich text and structured documents, but what separates it from most other markup languages is its features for hypertext and interactive documents. This section introduces the link (or hyperlink, or Web link), the basic hypertext construct. A link is a connection from one Web resource to another. Although a simple concept, the link has been one of the primary forces driving the success of the Web.

A link has two ends – called anchors – and a direction. The link starts at the “source” anchor and points to the “destination” anchor, which may be any Web resource (e.g., an image, a video clip, a sound bite, a program, an HTML document, an element within an HTML document, etc.).

1 Like

Yes “a” stands for anchor which is another name for hypertext link.

tag has a special meaning in html, it refers to the html markup element tags, such as p, a, ul, li, table, div, span, etc… In your attached image from the dev tools it is all of the elements that have the angled brackets around them.

You can find more on these through general sites like Mozilla web docs (HTML: HyperText Markup Language | MDN) or w3 schools (https://www.w3schools.com/). These resources should give you more than you ever wanted to know about the three languages (html, css, javascript) of web browsers.

While it would actually be easier to do what you want in JavaScript, I have modified your script to provide you with a function that will search the web page for a link containing a certain text.

set linkTextOfInterest to "Top"
set linkIndex to my getLinkIndex(linkTextOfInterest)

on getLinkIndex(linkTextOfInterest)
	
	tell application "Safari"
		
		tell document 1
			
			(* Number of anchors in the document *)
			set linkCount to do JavaScript "document.getElementsByTagName('a').length"
			
			set linkIndex to 0
			repeat with i from 0 to linkCount
				
				(* Text shown as the link in the document *)
				set linkInnerText to do JavaScript "document.getElementsByTagName('a')[" & i & "].innerText"
				
				if linkInnerText = linkTextOfInterest then
					set linkIndex to i
					exit repeat
				end if
				
			end repeat
		end tell
		
		return linkIndex
		
	end tell
	
end getLinkIndex
1 Like

Mockman. Thanks for the link. My search skills are not what they used to be.

Pseudotsuga. Thanks for all the great information. I tested your script on the MacScripter site and it returned the correct element number. I also tested it on some buttons on the Google News site, and it worked there as well. It appears that the innerText property is key to the script’s operation, and, for anyone interested, this property is defined as shown below. The length property is also useful to know.

The innerText property of the HTMLElement interface represents the rendered text content of a node and its descendants. As a getter, it approximates the text the user would get if they highlighted the contents of the element with the cursor and then copied it to the clipboard.

Just as an aside, linkCount at the beginning of the repeat loop might be changed to (linkCount - 1). Otherwise an error is thrown if a match is not found.

Yes, you are right. After warning you about zero-based collections I went ahead and ignored my own advice. Such is life…

Here is a revised version with the added correction and a try-error construct to catch any other potential errors.

set linkTextOfInterest to "Top"
set linkIndex to my getLinkIndex(linkTextOfInterest)

on getLinkIndex(linkTextOfInterest)
	
	try
		
		tell application "Safari"
			
			tell document 1
				
				(* Number of anchors in the document *)
				set linkCount to do JavaScript "document.getElementsByTagName('a').length"
				
				set linkIndex to 0
				repeat with i from 0 to linkCount - 1
					
					(* Text shown as the link in the document *)
					set linkInnerText to do JavaScript "document.getElementsByTagName('a')[" & i & "].innerText"
					
					if linkInnerText = linkTextOfInterest then
						set linkIndex to i
						exit repeat
					end if
					
				end repeat
				
			end tell
			
			return linkIndex
			
		end tell
		
	on error errMsg
		display alert errMsg
	end try
	
end getLinkIndex
1 Like

I’ve been testing my script in post 6 without issue. However, the situation might arise where the script would fail because the web page has not fully loaded. The following script should (hopefully) address this issue.

--test site is https://www.macscripter.net

set userName to "peavine"
set userPassword to "xxx" --not my real password
set delayLength to 0.5 --test different values
set the clipboard to userPassword

tell application "Safari"
	activate
	tell document 1
		set siteFound to false
		repeat 5 times --check if web site open by getting value of "Log In" button
			delay delayLength
			try
				do JavaScript "document.getElementsByClassName('d-button-label')[1].innerHTML;"
				set aCheck to result --only purpose is to force error if innerHTML value not returned
				set siteFound to true
				exit repeat
			end try
		end repeat
		if siteFound is false then display dialog "Web site not found" buttons {"OK"} cancel button 1 default button 1
		do JavaScript "document.getElementsByClassName('d-button-label')[1].click();" --click log in button
		delay delayLength
		do JavaScript "document.getElementById('login-account-name').value = '';" --empty user name
		do JavaScript "document.getElementById('login-account-name').value = 'peavine';" --enter user name
		delay delayLength
		do JavaScript "document.getElementById('login-account-password').value = '';" --empty password
		do JavaScript "document.getElementById('login-account-password').focus();" --move focus to password field
		delay delayLength
		pasteClipboard() of me --paste password
		delay delayLength
		do JavaScript "document.getElementById('login-button').click();" --click log in button
	end tell
end tell

on pasteClipboard()
	tell application "System Events" to keystroke "v" using {command down}
end pasteClipboard

This is a bit off-topic, but I’m working to learn JavaScript (primarily DOM) for use with AppleScripts and shortcuts, and I’m making good progress. However, I’ve encountered what is probably a simple issue, and I hoped someone could help me.

The following works as expected in a Run JavaScript on Active Safari Tab action in a shortcut:

let theLinks = [];
let theElements = document.querySelectorAll("a");
for (let anElement of theElements) {
    theLinks.push(
        anElement.href
    );
}
completion(theLinks);

However, I can’t get this to work in an AppleScript. Thanks for the help.

tell application "Safari" to tell document 1
	set theLinks to do JavaScript "let theLinks = [];
	let theElements = document.querySelectorAll('a');
	for (let anElement of theElements) {
		theLinks.push(
			anElement.href
		);
	}"
end tell
return theLinks

Hello @peavine :wave:

I’m by far no expert - but maybe you get no results because you didn’t used the whole JS code… I would give it a try and use the whole code with the do JavaScript command.

Greetings from Germany :de:

Tobias

This works:

tell application "Safari" to tell document 1
	set theLinks to do JavaScript "Array.from(document.querySelectorAll('a'), a => a.href)"
end tell
1 Like

Thanks Nr.5-need_input and roosterboy for the responses.

Roosterboy. I tested your suggestion and it works great. I have a lot to learn, but it always helps to have working examples like this.

Roosterboy’s script is surprisingly fast, taking about 5 milliseconds on complex websites with hundreds of links. An ASObjC filter is a useful addition, as demonstrated below. The ENDSWITH string comparison can be replaced with CONTAINS for a more generalized filter.

--test page is https://www.princexml.com/samples/

use framework "Foundation"
use scripting additions

set searchString to "pdf" --match is case insensitive
set matchingLinks to getMatchingLinks(searchString)

on getMatchingLinks(searchString)
	tell application "Safari" to tell document 1 to set theLinks to do JavaScript "Array.from(document.querySelectorAll('a'), a => a.href)" --works on frontmost Safari window
	set theLinks to current application's NSMutableArray's arrayWithArray:theLinks
	set thePredicate to current application's NSPredicate's predicateWithFormat_("(self ENDSWITH[c] %@)", searchString)
	return (theLinks's filteredArrayUsingPredicate:thePredicate) as list
end getMatchingLinks

Since you’re executing JavaScript anyway, it makes sense to do all the heavy lifting with JavaScript:

to getMatchingLinks(searchString)
        tell application "Safari" to tell the front document ¬
                to do JavaScript "Array.from( document.links,
                a => a.href ).filter( url => url.toLowerCase()
                .includes( " & searchString's quoted form & "
                .toLowerCase() ) );"
end getMatchingLinks

.includes(...) is similar to CONTAINS, or this could be substituted for .endsWith(...), .startsWith(...), or even .match(...) if you wanted to use regular expressions.

1 Like

I worked through CJK’s script for learning purposes, and I’ve included that below. The sources of this information are primarily the mdn and w3schools web sites.

I tested CJKs script against my edit of roosterboy’s script and both took 4 milliseconds on my test website. The returned results were also the same. CJK’s script does not require the Foundation framework and might be preferred for that reason.

tell application "Safari" to tell the front document to do JavaScript "Array.from(document.links, a => a.href).filter(url => url.toLowerCase().includes('pdf'.toLowerCase()));"

–The Array.from() method creates a new array from an iterable or array-like object (e.g. a NodeList)

–The document object represents any web page loaded in the browser and serves as an entry point into the web page’s content

–The links property returns a collection of all anchor elements in a document with a value for the href attribute

–The => arrow function expression is a compact alternative to a traditional function expression

–The a attribute is an anchor

–The href attribute specifies the location of a web resource

–The filter() method creates a new array of elements that pass a test provided by a function

–A url is a url

–The toLowerCase() method returns a string converted to lower case

–The includes() method determines whether an array includes a certain value among its entries, returning true or false as appropriate

1 Like