Click HTML element?

Is it possible to get BTT to click an HTML element? I want to click a particular button, but its position is dynamic so I can’t use that. I’d like to click it by ID, class, or contained text...

Have a look here: Can't execute JS in chrome or Safari

Sorry, couldn't get it to work...
To back up a bit, I'm trying to have a single keystroke:

  1. Open a particular URL (or better yet, loop through a list of URLs)
  2. Click on a button identified by the HTML inner-text

Entering this in my browser's javascript console works:

var xpath = "//a[contains(text(),'BTT')]";
var matchingElement = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
matchingElement.click();

How do I get BTT to accomplish this?

Look here and put your code into the „do JavaScript“ or „execute JavaScript“ section.

Thanks for the reply, Dirk.
I keep getting a syntax error that seems to indicate that "tell application..." is not valid:
Screen Shot 2022-02-03 at 6.59.19 PM

Try this in a AppleScript action:

Note: "You must enable the 'Allow JavaScript from Apple Events' option in Safari's Develop menu to use 'do JavaScript'." and replace "https://yourTargetUrL.com/" with your url!

set targetURL to "https://yourTargetUrL.com/"

tell application "Safari"
	repeat with t in tabs of windows
		tell t
			if URL starts with targetURL then
				do JavaScript "var xpath = '//a[contains(text(),'BTT')]';\nvar matchingElement = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;\nmatchingElement.click();"
				exit repeat
			end if
		end tell
	end repeat
end tell

Here is a script which opens the page and then executes the code.
You may have to adjust the loading time (delay).

set targetURL to "https://yourTargetUrL.com/"

tell application "Safari"
	open location targetURL
	delay 1 -- wait 1 second for page to load
	tell first window
		tell current tab
			do JavaScript "var xpath = '//a[contains(text(),'BTT')]';var matchingElement = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;matchingElement.click();"
		end tell
	end tell
end tell

Dirk,
You've gone above and beyond. THANKS!

FWIW, the code didn't work at first so I started poking around. When I copied it directly into the console in Safari, it threw an error because it was seeing 'BTT' as an identifier (though the single quotes worked fine in Brave). In Safari, I could use double quotes and it worked, but this refused to compile so I ended up needed to use escaped double quotes and this worked both in Safari & Brave and also compiled correctly in BTT!

Here's what I ended up with:

set targetURL to {BOT_URL}

tell application "Safari"
	open location targetURL
	delay 2.5 -- wait x seconds for page to load
	tell first window
		tell current tab
			do JavaScript "var xpath = '//a[contains(text(),\"BTT\")]';var matchingElement = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;matchingElement.click();"
		end tell
	end tell
end tell

You are right. The variable "xpath" in your JavaScript code is a string which contains a string as parameter. Therefore you have to escape it. I had not been able to test the JavaScript code myself, since I did not know the URL. But fine, if it works now.

Here is a all-round script without a fixed delay:

-- Open url in Safari and click first link that contains specific text

set targetURL to {BOT_URL}
set linkContains to "BTT"
set timeoutSecounds to 5

tell application "Safari"
	open location targetURL
	tell first window
		tell current tab
			set linkFound to false
			set timeoutDate to (current date) + timeoutSecounds
			repeat while linkFound is false and ((current date) < timeoutDate)
				set linkFound to (do JavaScript "var xpath = '//a[contains(text(),\"" & linkContains & "\")]';var matchingElement = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;(matchingElement != null) ? true: false;")
				delay 0.1
			end repeat
			if linkFound then
				do JavaScript "matchingElement.click();"
				return true
			end if
			return false
		end tell
	end tell
end tell

Wow, thanks Dirk!
So if I'm understanding this, it'll be faster yet less likely to "break" if my connection isn't always the same speed, right? I'll definitely give this a shot!

I'm am now trying to get a dynamic list of URLs to loop through, rather than having them hardcoded, since some will change on a daily basis. I'd like to have a "do JavaScript" statement gather the applicable values, concatenate the beginning and ending of the URL to that value, create an array, then send that list back to the parent AppleScript. Does that make sense?

In the screenshot below, I'd like to skip the value underlined in red (because of the "val zero" or "--" boxed in red), but gather the ones underlined in green into a variable. Each <row... on the current tab will have a single value to either skip or add to the array.

The other thing I've been trying to do is somehow reference the same Safari window throughout the script, regardless if I'm currently working with Safari and consequently make a different window "window 1" during while the script is running...

Yes, that's correct.

I rewrote the script once on JXA in the hope that "doJavaScript" might return JavaScript objects. Unfortunately this is not the case and only primitive types (strings, numbers, true/false, etc.) are supported. Furthermore, there is no good documentation for JXA.

Here is the script example (Change the language in BTT or Script editor to JavaScript!):

//debugger;
var safari = Application('Safari');

var myTab = openURL('https://www.google.com');

var loopCount = 0
while ((safari.doJavaScript("var target = document.getElementsByName('q')[0];(target != undefined);", { in: myTab })) == false && loopCount < 30)
{
	loopCount ++;
	delay(.1)
} 
safari.doJavaScript("target.value = 'BetterTouchTool';", { in: myTab })
safari.doJavaScript("document.getElementsByName('btnK')[0].click();", { in: myTab })

function openURL(url){
	if (safari.windows.length == 0){
		safari.Document().make();
	}
	var window = safari.windows[0];

	var tabIndex = -1;
	for (var index in window.tabs) {
		if (window.tabs[index].url().startsWith(url)) {
			tabIndex = index;
			break;
		}
	}
	if (tabIndex == -1) {
		tabIndex = window.tabs.push(safari.Tab ({url: url})) -1;
	}

	window.currentTab = safari.windows[0].tabs[tabIndex]
	return window.tabs[tabIndex]
}

Tip: You can with JXA debug AppleScript in Safari (you have to activate it in the developer menu and set a "debugger" statement).

As for your second question, I would rethink the approach. Programming a BOT is not easy.
Instead of executing the code via AppleScript, I would only open the page with it and inject a JavaScript with a Safari extension. Check out this:

Userscripts for Safari

Otherwise, there is a very good tool, which unfortunately has not been updated for years, but still works excellently:

Fake App

With this tool you can probably do anything you want and without a line of code:


It can also be controlled via AppleScript, but the tool is not cheap.

I hope this helps you a bit.

Dirk, I hear you about the need to rethink the approach...
Some of my main objectives are to have this run as unobtrusively as possible (preferably without so much as a screen flash, since this will be running while I'm using the computer for other things as well!), and also be robust in terms of not breaking if I'm currently using Safari, for example.

If I were to use a Safari extension to inject a JavaScript, would this allow me to stay completely discrete and also to avoid problems if I were working with Safari at the time (where "current tab" can be the wrong thing!)?

Concerning Fake App, do you know whether it's possible to export the workflow as an AppleScript to be used externally to Fake?
Since it looks like it hasn't had any updates in 11 years :flushed:, I don't really want to consider it if I can't work with the resulting workflow outside of the App itself.

No. The Fake App is a complete standalone Browser App based on the WebKit Framework which means that it always uses the latest browser engine from macOS. The UserAgent can be customized, so the missing update (the last one was in November 2016) only affects the look. But yes, it is over 5 years old.
The big advantage is that you can do everything with Drag & Drop and without Scripting. You can change and test the workflow at any time. It is an "Automator for websites".
I have not seen anything comparable so far.

Yes. The script will be injected when the page is loaded based on what you specify in the header. Here is an example:

// ==UserScript==
// @name        Wikipedia
// @description Dark Mode
// @match       *://*.wikipedia.org/*
// ==/UserScript==

It runs only in the tab in which the page is loaded.

In this case, it would make sense if the whole thing runs in a completely standalone app that you can hide. Take a look at:

Fluid

With this you can create your own Web Apps and include your own Userscripts and Userstyles. It's from the same developer, but was last updated in October 2018 and costs only $5. It's again based on WebKit and the same applies as I said about Fake.
Unfortunately, I don't know anything better for this either.

PS: For Fake and Fluid there is a trial version available. Userscripts for Safari is free.