[Library] Chrome.ahk - Automate Google Chrome using native AutoHotkey. No IE!

Post your working scripts, libraries and tools for AHK v1.1 and older
fenchai
Posts: 292
Joined: 28 Mar 2016, 07:57

Re: [Library] Chrome.ahk - Automate Google Chrome using native AutoHotkey. No Selenium!

19 Mar 2018, 14:32

Shadowpheonix wrote:
fenchai wrote:Is there any reason everytime chrome opens, it shows something about windows defender wanting to reset my chrome preferences and adobe and google drive keep wanting to install? this is weird. :shock:
This sounds like a problem with either your Chrome profile or with an extension/plugin you have installed in Chrome.
If you haven't already done so, try running with a brand new profile in Chrome and see if the issue still occurs. I am not sure what else to suggest if that fails.
True, the problem was on my end. I tried the same on my working PC (without any profile) and it works just fine. So I always need to make a new profile for this to work? it doesn't seem very time saving if I have to do this every time. hahah I guess i will have to wait until we get it to work on other than empty profiles.
Shadowpheonix
Posts: 1259
Joined: 16 Apr 2015, 09:41

Re: [Library] Chrome.ahk - Automate Google Chrome using native AutoHotkey. No Selenium!

19 Mar 2018, 15:01

fenchai wrote:
Shadowpheonix wrote:
fenchai wrote:Is there any reason everytime chrome opens, it shows something about windows defender wanting to reset my chrome preferences and adobe and google drive keep wanting to install? this is weird. :shock:
This sounds like a problem with either your Chrome profile or with an extension/plugin you have installed in Chrome.
If you haven't already done so, try running with a brand new profile in Chrome and see if the issue still occurs. I am not sure what else to suggest if that fails.
True, the problem was on my end. I tried the same on my working PC (without any profile) and it works just fine. So I always need to make a new profile for this to work? it doesn't seem very time saving if I have to do this every time. hahah I guess i will have to wait until we get it to work on other than empty profiles.
A new profile should not be needed every time. I use the same profile every time on my system and Chrome gives me no issues.
If you are getting the same issue the second time you use a profile, then I suspect you are logging into your Google account and Chrome is having trouble syncing extensions, bookmarks, etcetera. Try making a new profile and disabling the sync on it. If that clears up the issue, enable one type of sync and try again - keep enabling sync types & relaunching with that profile until you find the one that triggers the issue.
User avatar
Joe Glines
Posts: 770
Joined: 30 Sep 2013, 20:49
Location: Dallas
Contact:

Re: [Library] Chrome.ahk - Automate Google Chrome using native AutoHotkey. No Selenium!

20 Mar 2018, 07:51

Shadowpheonix wrote:
Joe Glines wrote:this still leaves me clueless as to how to read an existing value from a web page in Chrome without having to install Selenium. :lol:
Sorry- I thought you were trying to set a value. I have something similar for getting values but am busy today preparing for our webinar on Neural Networks with AutoHotkey. I'll try and get to it later this week / this weekend.

Regards,
Joe
Sign-up for the 🅰️HK Newsletter

ImageImageImageImage:clap:
AHK Tutorials:Web Scraping | | Webservice APIs | AHK and Excel | Chrome | RegEx | Functions
Training: AHK Webinars Courses on AutoHotkey :ugeek:
YouTube

:thumbup: Quick Access Popup, the powerful Windows folders, apps and documents launcher!
geek
Posts: 1052
Joined: 02 Oct 2013, 22:13
Location: GeekDude
Contact:

Re: [Library] Chrome.ahk - Automate Google Chrome using native AutoHotkey. No Selenium!

21 Mar 2018, 16:59

Chrome.ahk v1.1 has been released

Please see the original post for a link to the release and revision history pages.

Changes
  • class Chrome's constructor now accepts additional flags for chrome
  • Pages are no longer connected to using Chrome.GetTab()
Additions
  • New methods for finding the right page to connect to
  • New method Chrome.Kill() for ending the Chrome process
  • New example script ExportPDF.ahk demonstrating headless chrome and PDF exports.
User avatar
Joe Glines
Posts: 770
Joined: 30 Sep 2013, 20:49
Location: Dallas
Contact:

Re: [Library] Chrome.ahk - Automate Google Chrome using native AutoHotkey. No Selenium!

21 Mar 2018, 17:01

Awesome stuff GeekDude! :)
Sign-up for the 🅰️HK Newsletter

ImageImageImageImage:clap:
AHK Tutorials:Web Scraping | | Webservice APIs | AHK and Excel | Chrome | RegEx | Functions
Training: AHK Webinars Courses on AutoHotkey :ugeek:
YouTube

:thumbup: Quick Access Popup, the powerful Windows folders, apps and documents launcher!
Shadowpheonix
Posts: 1259
Joined: 16 Apr 2015, 09:41

Re: [Library] Chrome.ahk - Automate Google Chrome using native AutoHotkey. No Selenium!

22 Mar 2018, 13:15

GeekDude wrote:If you have a chrome instance already running in debug mode, you can skip the initialization of the Chrome class and just call Chrome.GetTab() directly. For example:

Code: Select all

#Include Chrome.ahk

TabInst := Chrome.GetTab()
TabInst.Evaluate("alert('hi!');")
What is the replacement for this process with v1.1? GetTab() no longer exists in the class, and PageInst := Chrome.GetPage() returns a "connection with the server could not be established" error.
geek
Posts: 1052
Joined: 02 Oct 2013, 22:13
Location: GeekDude
Contact:

Re: [Library] Chrome.ahk - Automate Google Chrome using native AutoHotkey. No Selenium!

22 Mar 2018, 23:55

ramonstart wrote:Hello

Why does the Jxon_Dump method return the "menor preu00E7o do livro" value when the correct text is "menor preço do livro"?

I'm using the call below to bring the content of the list that is in the url https://www.estantevirtual.com.br/livro ... 20Kiyosaki

Would you help me?
I would have expected it to output \u00E7 with a backslash. The sequence \u marks the next four characters as a hexadecimal representation of a Unicode character's code point. Code point 00E7 corresponds to ç, which can be seen on Wikipedia's list of Unicode characters.

Image

Shadowpheonix wrote:
Joe Glines wrote:
Shadowpheonix wrote:In the "pastebin.ahk" example that GeekDude was nice enough to include in the release package, the Tab.Call("DOM.setAttributeValue", {"nodeId": NameNode.NodeId, "name": "value", "value": "ChromeBot"}) line assigns a value to an input field. How would I go about retrieving the value after it was assigned?
I don't have an example I can post right now but I "solved this" by inserting Java script.

If you happen to come up with an example you can post, it would be most appreciated. :D
Something like MsgBox, % PageInst.Evaluate("document.querySelector('input[name=desc]').value").value should work.

fenchai wrote:Is there any reason everytime chrome opens, it shows something about windows defender wanting to reset my chrome preferences and adobe and google drive keep wanting to install? this is weird. :shock:
You may be able to disable those warnings by providing the --no-first-run and --disable-extensions flags in the Flags parameter of new Chrome(ProfilePath, URL, Flags) in Chrome.ahk_v1.1. For example, ChromeInst := new Chrome("ChromeProfile",, "--no-first-run --disable-extensions")

Shadowpheonix wrote:
GeekDude wrote:If you have a chrome instance already running in debug mode, you can skip the initialization of the Chrome class and just call Chrome.GetTab() directly. For example:

Code: Select all

#Include Chrome.ahk

TabInst := Chrome.GetTab()
TabInst.Evaluate("alert('hi!');")
What is the replacement for this process with v1.1? GetTab() no longer exists in the class, and PageInst := Chrome.GetPage() returns a "connection with the server could not be established" error.
You're meant to create an instance of the Chrome class before calling GetTab()/GetPage(). If you're already running a copy of chrome with debugging enabled I can see how it could have worked with Chrome.ahk v1.0, but not anymore with v1.1.

If you don't want to launch a new instance of chrome to connect to, you could probably set Chrome.DebugPort := 9222 before calling Chrome.GetPage() and it would behave like you were expecting.

kunkel321 wrote:Thanks for your work on this, GeekDude! It's a bit above my ability to fully understand, but I wonder: Would it be possible to use this for setting the default printer in Chrome? I have a nice AHK script that (at system startup) reads my IP address, then sets my default Windows printer. Chrome doesn't use the Windows default printer though. It merely reverts to whatever printer was last used.
As far as I can tell, there is no way to change the printer settings through this interface.
Shadowpheonix
Posts: 1259
Joined: 16 Apr 2015, 09:41

Re: [Library] Chrome.ahk - Automate Google Chrome using native AutoHotkey. No Selenium!

23 Mar 2018, 00:21

GeekDude wrote:
Shadowpheonix wrote:
Joe Glines wrote:
Shadowpheonix wrote:In the "pastebin.ahk" example that GeekDude was nice enough to include in the release package, the Tab.Call("DOM.setAttributeValue", {"nodeId": NameNode.NodeId, "name": "value", "value": "ChromeBot"}) line assigns a value to an input field. How would I go about retrieving the value after it was assigned?
I don't have an example I can post right now but I "solved this" by inserting Java script.
If you happen to come up with an example you can post, it would be most appreciated. :D
Something like MsgBox, % PageInst.Evaluate("document.querySelector('input[name=desc]').value").value should work.
Shadowpheonix wrote:
GeekDude wrote:If you have a chrome instance already running in debug mode, you can skip the initialization of the Chrome class and just call Chrome.GetTab() directly. For example:

Code: Select all

#Include Chrome.ahk

TabInst := Chrome.GetTab()
TabInst.Evaluate("alert('hi!');")
What is the replacement for this process with v1.1? GetTab() no longer exists in the class, and PageInst := Chrome.GetPage() returns a "connection with the server could not be established" error.
You're meant to create an instance of the Chrome class before calling GetTab()/GetPage(). If you're already running a copy of chrome with debugging enabled I can see how it could have worked with Chrome.ahk v1.0, but not anymore with v1.1.

If you don't want to launch a new instance of chrome to connect to, you could probably set Chrome.DebugPort := 9222 before calling Chrome.GetPage() and it would behave like you were expecting.
Thank you! Both work perfectly, and will help me solve some long standing issues I have had with automating a few web interactions for my work. :superhappy:
User avatar
Joe Glines
Posts: 770
Joined: 30 Sep 2013, 20:49
Location: Dallas
Contact:

Re: [Library] Chrome.ahk - Automate Google Chrome using native AutoHotkey. No Selenium!

25 Mar 2018, 07:28

Shadowpheonix wrote:
Joe Glines wrote:However, this still leaves me clueless as to how to read an existing value from a web page in Chrome without having to install Selenium. :lol:
I posted my function for getting text / data from a page with Chrome & AutoHotkey. Check it out and the below video walking through how to use it.
Sign-up for the 🅰️HK Newsletter

ImageImageImageImage:clap:
AHK Tutorials:Web Scraping | | Webservice APIs | AHK and Excel | Chrome | RegEx | Functions
Training: AHK Webinars Courses on AutoHotkey :ugeek:
YouTube

:thumbup: Quick Access Popup, the powerful Windows folders, apps and documents launcher!
Freire
Posts: 16
Joined: 10 Dec 2017, 11:30

Re: [Library] Chrome.ahk - Automate Google Chrome using native AutoHotkey. No Selenium!

28 Mar 2018, 08:10

GeekDude,

Thanks a lot for this tool. This is way better than working with IE or the selenium basic with ahk.

There is a minor bug that I found, when opening multiple url's you have to wait for like 700 ms for the about:blank on page before calling Chrome.GetPage() or it will use the old tab opened to return the object.
Guest

Re: [Library] Chrome.ahk - Automate Google Chrome using native AutoHotkey. No Selenium!

04 Apr 2018, 07:01

@joeglines

I haven't studied your Chm_Set() function very closely yet but will you be adding "frame support" to Chm_Set() so you can control forms in say a "content" frame (or frame[2]) document.parentwindow.frames("content").document. :?:
User avatar
Joe Glines
Posts: 770
Joined: 30 Sep 2013, 20:49
Location: Dallas
Contact:

Re: [Library] Chrome.ahk - Automate Google Chrome using native AutoHotkey. No Selenium!

04 Apr 2018, 07:23

I wasn't planning on it but that's something that, theoretically, shouldn't be too hard for you to do... I don't anticipate actually using the chrome functionality myself as I do all of my scraping with IE. I'm just excited that the basics are available for Chrome so people can automate logging-in, etc.
Sign-up for the 🅰️HK Newsletter

ImageImageImageImage:clap:
AHK Tutorials:Web Scraping | | Webservice APIs | AHK and Excel | Chrome | RegEx | Functions
Training: AHK Webinars Courses on AutoHotkey :ugeek:
YouTube

:thumbup: Quick Access Popup, the powerful Windows folders, apps and documents launcher!
User avatar
Lateralus138
Posts: 49
Joined: 30 Aug 2015, 20:52
Location: Decatur, IL.
Contact:

Re: [Library] Chrome.ahk - Automate Google Chrome using native AutoHotkey. No Selenium!

05 Apr 2018, 15:30

Much appreciated!!! I've been expanding (learning) my AutoHotkey skills more in browser com objects lately and testing and looking through scripts and functions like this help me understand how things work a lot better than plain documentation usually does.

K̴̡̛̻̮̼͕̬̑̋̀̂͆͛̍̑͢ȩ̮̞͍̩̯̋̈͒͌̕ę̶͓̗͖͔̹̪͗̂̈͛̓͘p̠͉̙̟̒̊͌̐͘͘͟͡͞ S̸͖̖̮̞̥͇̖̓̌͛̽̿̓̊̓̾̚͜w͇̮͓̱͇̘̯͆̓͑̋̇̉͜͝i̢͔̝̳̻̱̋̾͐̾͗͊̀̕͜͡͡n̷̡͔̦̤̝̼̩̎͌̈́̀͛̄͆̎͠ǵ̸̘̝̭̦̠̗͖͌͐͑̑̿̅̈͜͜ḯ̡̬̥̙̩̼̪̑͆̿̌́n̛̼͎̲̬͇̲͉̗̞͊̓̃̂̈͝g̸͕̜͖̪͉͔̩̓̃̀̃͌̑̋̕͘.̪̜̜̜̯̂͂̈́͛̆͗̇̍̇.̟͔͍̙̜̫̗̂̿͛͋͋̈́̾̾̿͑.̡̣̟̝̭͉̦̪́̓̀͛̑̓̐̈͘͘

KusochekDobra
Posts: 38
Joined: 25 Apr 2016, 18:00

Re: [Library] Chrome.ahk - Automate Google Chrome using native AutoHotkey. No Selenium!

11 Apr 2018, 18:13

It's a good job!!! Great thanks!!!

But how can i add event listener in JavaScript-code, which launches the functions in AHK-code?
Next simple example shows the coordinates of a click in a fixed div-block, if click on the button "Click inform" when the page loads:

Code: Select all

#NoEnv
#SingleInstance, Force
SetBatchLines, -1

#Include ../Chrome.ahk

; --- Create a new Chrome instance ---

addr1 := "https://autohotkey.com/"
addr2 := "https://autohotkey.com/boards/viewtopic.php?f=6&t=42890&sid=100a6baa98d3de10e3cdf42616ae10d9"
chPath := "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe"
profileName := "ChromeProfile"
recycleProfile := true
showEvalResult := false

FileCreateDir, %profileName%
ChromeInst := new Chrome(profileName, addr2,, chPath)

informerCode :=
	(LTrim Join`s
	"function handlerF(ev) {
		info.innerHTML = ``${ev.clientX}x ${ev.clientY}y``;
	}

	var body = document.querySelector('body');
	var info = document.createElement('div');
	info.id = 'info';
	info.style.position = 'fixed';
	info.style.top = '10px';
	info.style.right = '10px';
	info.style.width = '200px';
	info.style.border = '2px solid #fff';
	info.style.color = '#fff';
	info.style.fontWeight = 'bold';
	info.style.fontSize = '20px';
	info.style.textShadow = '1px 1px 1px #000, 1px -1px 1px #000, -1px -1px 1px #000, -1px 1px 1px #000';
	info.style.textAlign = 'center';
	body.appendChild(info);
	info.innerHTML = 'Click on the page';

	body.addEventListener('click', handlerF);"
	)

; --- Connect to the page ---

if !(PageInst := ChromeInst.GetPage()) {
	MsgBox, Could not retrieve page!
	ChromeInst.Kill()
	GoSub, CloseMe
} else {
	Gui,Input: Margin, 10, 10
	Gui,Input: +Hwndinput_h
	Gui,Input: Add, Button,gEvalMe,Run
	Gui,Input: Add, Text,x+10 yp+5,Run a JS-script on the page:
	Gui,Input: Font, s10 Bold, Consolas
	Gui,Input: Add, Edit,xm y+7 w600 r20 vmyCode cBlue,alert('Hello World!!!');
	Gui,Input: Font
	Gui,Input: Add, Button,xm y+5 gAddInform,Click inform
	Gui,Input: Add, Button,x+10 gCloseMe,Close
	Gui,Input: Show,,Input
}
return
EvalMe:
	Gui,Input: Submit, NoHide
	if (myCode == "")
		return
	GoSub, EvaluateHim
return
AddInform:
	myCode := informerCode
	GoSub, EvaluateHim
return
EvaluateHim:
	try
		Result := PageInst.Evaluate(myCode)
	catch e
	{
		MsgBox, % "Exception encountered in " e.What ":`n`n"
		. e.Message "`n`n"
		. "Specifically:`n`n"
		. Chrome.Jxon_Dump(Chrome.Jxon_Load(e.Extra), "`t")
	} if (showEvalResult)
		MsgBox, % "Result:`n" Chrome.Jxon_Dump(Result, "`t")
return
CloseMe:
InputGuiClose:

	; --- Close the Chrome instance ---
	
	try
		PageInst.Call("Browser.close") ; Fails when running headless
	catch
		ChromeInst.Kill()
	PageInst.Disconnect()
	
	fPath := Format("{1}\{2}",A_ScriptDir,profileName)
	if (recycleProfile) {
		FileRemoveDir,% fPath, 1
		i := 0
		while (i < 10, i++) {
			Sleep, 1000
			if (FileExist(fPath))
				FileRemoveDir,% fPath, 1
			else
				break
		}
	}
	ExitApp

How to call a AHK-function that shows this coordinates of a click in "MsgBox"?

Update:
If I understand correctly, the ActiveX control in the object of the class acts as a server for the browser. Then how do I send a message from the browser page?
Then it could be processed in "ws.onmessage". Right?
KusochekDobra
Posts: 38
Joined: 25 Apr 2016, 18:00

Re: [Library] Chrome.ahk - Automate Google Chrome using native AutoHotkey. No Selenium!

18 Apr 2018, 17:26

The only trouble-free solution that we managed to find is to activate the console domain:
PageInst.Call("Console.enable")
And catch incoming messages "Console.messageAdded"
I had to change the "Chrome" class a little, adding the sixth parameter to the call constructor, here and there to transfer it further for the nested class "Page", and rename it to "PageBase" and extend him, redefining the constructor and the method "Event":

Code: Select all

; Chrome.ahk v1.1
; Copyright GeekDude 2018
; https://github.com/G33kDude/Chrome.ahk

class Chrome
{
	/*
		Escape a string in a manner suitable for command line parameters
	*/
	CliEscape(Param)
	{
		return """" RegExReplace(Param, "(\\*)""", "$1$1\""") """"
	}
	
	/*
		ProfilePath - Path to the user profile directory to use. Will use the standard if left blank.
		URL         - The page for Chrome to load when it opens
		Flags       - Additional flags for chrome when launching
		ChromePath  - Path to chrome.exe, will detect from start menu when left blank
		DebugPort   - What port should Chrome's remote debugging server run on
	*/
	__New(ProfilePath:="", URL:="about:blank", Flags:="", ChromePath:="", DebugPort:=9222, funcName := "")
	{
		this.funcName := funcName
		; Verify ProfilePath
		if (ProfilePath != "" && !InStr(FileExist(ProfilePath), "D"))
			throw Exception("The given ProfilePath does not exist")
		this.ProfilePath := ProfilePath
		
		; Verify ChromePath
		; TODO: Perform a more rigorous search for Chrome
		if (ChromePath == "")
			FileGetShortcut, %A_StartMenuCommon%\Programs\Google Chrome.lnk, ChromePath
		if !FileExist(ChromePath)
			throw Exception("Chrome could not be found")
		this.ChromePath := ChromePath
		
		; Verify DebugPort
		if DebugPort is not integer
			throw Exception("DebugPort must be a positive integer")
		if (DebugPort <= 0)
			throw Exception("DebugPort must be a positive integer")
		this.DebugPort := DebugPort
		
		; TODO: Support an array of URLs
		Run, % this.CliEscape(ChromePath)
		. " --remote-debugging-port=" this.DebugPort
		. (ProfilePath ? " --user-data-dir=" this.CliEscape(ProfilePath) : "")
		. (Flags ? " " Flags : "")
		. (URL ? " " this.CliEscape(URL) : "")
		,,, OutputVarPID
		this.PID := OutputVarPID
	}
	
	/*
		End Chrome by terminating the process.
	*/
	Kill()
	{
		Process, Close, % this.PID
	}
	
	/*
		Queries chrome for a list of pages that expose a debug interface.
		In addition to standard tabs, these include pages such as extension
		configuration pages.
	*/
	GetPageList()
	{
		http := ComObjCreate("WinHttp.WinHttpRequest.5.1")
		http.open("GET", "http://127.0.0.1:" this.DebugPort "/json")
		http.send()
		return this.Jxon_Load(http.responseText)
	}
	
	/*
		Returns a connection to the debug interface of a page that matches the
		provided criteria. When multiple pages match the criteria, they appear
		ordered by how recently the pages were opened.
		
		Key       - The key from the page list to search for, such as "url" or "title"
		Value     - The value to search for in the provided key
		MatchMode - What kind of search to use, such as "exact", "contains", "startswith", or "regex"
		Index     - If multiple pages match the given criteria, which one of them to return
	*/
	GetPageBy(Key, Value, MatchMode:="exact", Index:=1)
	{
		Count := 0
		for n, PageData in this.GetPageList()
		{
			if (((MatchMode = "exact" && PageData[Key] = Value) ; Case insensitive
				|| (MatchMode = "contains" && InStr(PageData[Key], Value))
				|| (MatchMode = "startswith" && InStr(PageData[Key], Value) == 1)
				|| (MatchMode = "regex" && PageData[Key] ~= Value))
				&& ++Count == Index)
				return new this.Page(PageData.webSocketDebuggerUrl, this.funcName)
		}
	}
	
	/*
		Shorthand for GetPageBy("url", Value, "startswith")
	*/
	GetPageByURL(Value, MatchMode:="startswith", Index:=1)
	{
		this.GetPageBy("url", Value, MatchMode, Index)
	}
	
	/*
		Shorthand for GetPageBy("title", Value, "startswith")
	*/
	GetPageByTitle(Value, MatchMode:="startswith", Index:=1)
	{
		this.GetPageBy("title", Value, MatchMode, Index)
	}
	
	/*
		Shorthand for GetPageBy("type", Type, "exact")
		
		The default type to search for is "page", which is the visible area of
		a normal Chrome tab.
	*/
	GetPage(Index:=1, Type:="page")
	{
		return this.GetPageBy("type", Type, "exact", Index)
	}
	
	/*
		Connects to the debug interface of a page given its WebSocket URL.
	*/
	class PageBase
	{
		Connected := False
		ID := 0
		Responses := []
		
		/*
			wsurl - The desired page's WebSocket URL
		*/
		__New(wsurl)
		{
			this.BoundKeepAlive := this.Call.Bind(this, "Browser.getVersion",, False)
			
			; TODO: Throw exception on invalid objects
			if IsObject(wsurl)
				wsurl := wsurl.webSocketDebuggerUrl
			
			wsurl := StrReplace(wsurl, "localhost", "127.0.0.1")
			this.ws := {"base": this.WebSocket, "_Event": this.Event, "Parent": this}
			this.ws.__New(wsurl)
			
			while !this.Connected
				Sleep, 50
		}
		
		/*
			Calls the specified endpoint and provides it with the given
			parameters.
			
			DomainAndMethod - The endpoint domain and method name for the
				endpoint you would like to call. For example:
				PageInst.Call("Browser.close")
				PageInst.Call("Schema.getDomains")
			
			Params - An associative array of parameters to be provided to the
				endpoint. For example:
				PageInst.Call("Page.printToPDF", {"scale": 0.5 ; Numeric Value
				, "landscape": Chrome.Jxon_True() ; Boolean Value
				, "pageRanges: "1-5, 8, 11-13"}) ; String value
				PageInst.Call("Page.navigate", {"url": "https://autohotkey.com/"})
			
			WaitForResponse - Whether to block until a response is received from
				Chrome, which is necessary to receive a return value, or whether
				to continue on with the script without waiting for a response.
		*/
		Call(DomainAndMethod, Params:="", WaitForResponse:=True)
		{
			if !this.Connected
				throw Exception("Not connected to tab")
			
			; Use a temporary variable for ID in case more calls are made
			; before we receive a response.
			ID := this.ID += 1
			this.ws.Send(Chrome.Jxon_Dump({"id": ID
			, "params": Params ? Params : {}
			, "method": DomainAndMethod}))
			
			if !WaitForResponse
				return
			
			; Wait for the response
			this.responses[ID] := False
			while !this.responses[ID]
				Sleep, 50
			
			; Get the response, check if it's an error
			response := this.responses.Delete(ID)
			if (response.error)
				throw Exception("Chrome indicated error in response",, Chrome.Jxon_Dump(response.error))
			
			return response.result
		}
		
		/*
			Run some JavaScript on the page. For example:
			
			PageInst.Evaluate("alert(""I can't believe it's not IE!"");")
			PageInst.Evaluate("document.getElementsByTagName('button')[0].click();")
		*/
		Evaluate(JS)
		{
			response := this.Call("Runtime.evaluate",
			( LTrim Join
			{
				"expression": JS,
				"objectGroup": "console",
				"includeCommandLineAPI": Chrome.Jxon_True(),
				"silent": Chrome.Jxon_False(),
				"returnByValue": Chrome.Jxon_False(),
				"userGesture": Chrome.Jxon_True(),
				"awaitPromise": Chrome.Jxon_False()
			}
			))
			
			if (response.exceptionDetails)
				throw Exception(response.result.description,, Chrome.Jxon_Dump(response.exceptionDetails))
			
			return response.result
		}
		
		/*
			Waits for the page's readyState to match the DesiredState.
			
			DesiredState - The state to wait for the page's ReadyState to match
			Interval     - How often it should check whether the state matches
		*/
		WaitForLoad(DesiredState:="complete", Interval:=100)
		{
			while this.Evaluate("document.readyState").value != DesiredState
				Sleep, Interval
		}
		
		/*
			Internal function triggered when the script receives a message on
			the WebSocket connected to the page.
		*/
		Event(EventName, Event)
		{
			; If it was called from the WebSocket adjust the class context
			if this.Parent
				this := this.Parent
			
			; TODO: Handle Error events
			if (EventName == "Open")
			{
				this.Connected := True
				BoundKeepAlive := this.BoundKeepAlive
				SetTimer, %BoundKeepAlive%, 15000
			}
			else if (EventName == "Message")
			{
				data := Chrome.Jxon_Load(Event.data)
				if this.responses.HasKey(data.ID)
					this.responses[data.ID] := data
			}
			else if (EventName == "Close")
			{
				this.Disconnect()
			}
		}
		
		/*
			Disconnect from the page's debug interface, allowing the instance
			to be garbage collected.
			
			This method should always be called when you are finished with a
			page or else your script will leak memory.
		*/
		Disconnect()
		{
			if !this.Connected
				return
			
			this.Connected := False
			this.ws.Delete("Parent")
			this.ws.Disconnect()
			
			BoundKeepAlive := this.BoundKeepAlive
			SetTimer, %BoundKeepAlive%, Delete
			this.Delete("BoundKeepAlive")
		}
		
		class WebSocket
		{
			__New(WS_URL)
			{
				static wb
				
				; Create an IE instance
				Gui, +hWndhOld
				Gui, New, +hWndhWnd
				this.hWnd := hWnd
				Gui, Add, ActiveX, vWB, Shell.Explorer
				Gui, %hOld%: Default
				
				; Write an appropriate document
				WB.Navigate("about:<!DOCTYPE html><meta http-equiv='X-UA-Compatible'"
				. "content='IE=edge'><body></body>")
				while (WB.ReadyState < 4)
					sleep, 50
				this.document := WB.document
				
				; Add our handlers to the JavaScript namespace
				this.document.parentWindow.ahk_savews := this._SaveWS.Bind(this)
				this.document.parentWindow.ahk_event := this._Event.Bind(this)
				this.document.parentWindow.ahk_ws_url := WS_URL
				
				; Add some JavaScript to the page to open a socket
				Script := this.document.createElement("script")
				Script.text := "ws = new WebSocket(ahk_ws_url);`n"
				. "ws.onopen = function(event){ ahk_event('Open', event); };`n"
				. "ws.onclose = function(event){ ahk_event('Close', event); };`n"
				. "ws.onerror = function(event){ ahk_event('Error', event); };`n"
				. "ws.onmessage = function(event){ ahk_event('Message', event); };"
				this.document.body.appendChild(Script)
			}
			
			; Called by the JS in response to WS events
			_Event(EventName, Event)
			{
				this["On" EventName](Event)
			}
			
			; Sends data through the WebSocket
			Send(Data)
			{
				this.document.parentWindow.ws.send(Data)
			}
			
			; Closes the WebSocket connection
			Close(Code:=1000, Reason:="")
			{
				this.document.parentWindow.ws.close(Code, Reason)
			}
			
			; Closes and deletes the WebSocket, removing
			; references so the class can be garbage collected
			Disconnect()
			{
				if this.hWnd
				{
					this.Close()
					Gui, % this.hWnd ": Destroy"
					this.hWnd := False
				}
			}
		}
	}
	Class Page Extends Chrome.PageBase
	{
		__New(wsurl, funcName)
		{
			(funcName && this.bindedFunction := Func(funcName))
			this.BoundKeepAlive := this.Call.Bind(this, "Browser.getVersion",, False)
			
			; TODO: Throw exception on invalid objects
			if IsObject(wsurl)
				wsurl := wsurl.webSocketDebuggerUrl
			
			wsurl := StrReplace(wsurl, "localhost", "127.0.0.1")
			this.ws := {"base": this.WebSocket, "_Event": this.Event, "Parent": this}
			this.ws.__New(wsurl)
			
			while !this.Connected
				Sleep, 50
		}
		Event(EventName, Event)
		{
			; If it was called from the WebSocket adjust the class context
			if this.Parent
				this := this.Parent
			
			; TODO: Handle Error events
			if (EventName == "Open")
			{
				this.Connected := True
				BoundKeepAlive := this.BoundKeepAlive
				SetTimer, %BoundKeepAlive%, 15000
			}
			else if (EventName == "Message")
			{
				data := Chrome.Jxon_Load(Event.data)
				if (this.bindedFunction && data.method == "Console.messageAdded" && (msg := data.params.message).level == "info")
					this.bindedFunction.Call(msg.text)
				if this.responses.HasKey(data.ID)
					this.responses[data.ID] := data
			}
			else if (EventName == "Close")
			{
				this.Disconnect()
			}
		}
	}
	Jxon_Load(ByRef src, args*)
	{
		static q := Chr(34)
		
		key := "", is_key := false
		stack := [ tree := [] ]
		is_arr := { (tree): 1 }
		next := q . "{[01234567890-tfn"
		pos := 0
		while ( (ch := SubStr(src, ++pos, 1)) != "" )
		{
			if InStr(" `t`n`r", ch)
				continue
			if !InStr(next, ch, true)
			{
				ln := ObjLength(StrSplit(SubStr(src, 1, pos), "`n"))
				col := pos - InStr(src, "`n",, -(StrLen(src)-pos+1))
				
				msg := Format("{}: line {} col {} (char {})"
				,   (next == "")      ? ["Extra data", ch := SubStr(src, pos)][1]
				: (next == "'")     ? "Unterminated string starting at"
				: (next == "\")     ? "Invalid \escape"
				: (next == ":")     ? "Expecting ':' delimiter"
				: (next == q)       ? "Expecting object key enclosed in double quotes"
				: (next == q . "}") ? "Expecting object key enclosed in double quotes or object closing '}'"
				: (next == ",}")    ? "Expecting ',' delimiter or object closing '}'"
				: (next == ",]")    ? "Expecting ',' delimiter or array closing ']'"
				: [ "Expecting JSON value(string, number, [true, false, null], object or array)"
				, ch := SubStr(src, pos, (SubStr(src, pos)~="[\]\},\s]|$")-1) ][1]
				, ln, col, pos)
				
				throw Exception(msg, -1, ch)
			}
			
			is_array := is_arr[obj := stack[1]]
			
			if i := InStr("{[", ch)
			{
				val := (proto := args[i]) ? new proto : {}
				is_array? ObjPush(obj, val) : obj[key] := val
				ObjInsertAt(stack, 1, val)
				
				is_arr[val] := !(is_key := ch == "{")
				next := q . (is_key ? "}" : "{[]0123456789-tfn")
			}
			
			else if InStr("}]", ch)
			{
				ObjRemoveAt(stack, 1)
				next := stack[1]==tree ? "" : is_arr[stack[1]] ? ",]" : ",}"
			}
			
			else if InStr(",:", ch)
			{
				is_key := (!is_array && ch == ",")
				next := is_key ? q : q . "{[0123456789-tfn"
			}
			
			else ; string | number | true | false | null
			{
				if (ch == q) ; string
				{
					i := pos
					while i := InStr(src, q,, i+1)
					{
						val := StrReplace(SubStr(src, pos+1, i-pos-1), "\\", "\u005C")
						static end := A_AhkVersion<"2" ? 0 : -1
						if (SubStr(val, end) != "\")
							break
					}
					if !i ? (pos--, next := "'") : 0
						continue
					
					pos := i ; update pos
					
					val := StrReplace(val,    "\/",  "/")
					, val := StrReplace(val, "\" . q,    q)
					, val := StrReplace(val,    "\b", "`b")
					, val := StrReplace(val,    "\f", "`f")
					, val := StrReplace(val,    "\n", "`n")
					, val := StrReplace(val,    "\r", "`r")
					, val := StrReplace(val,    "\t", "`t")
					
					i := 0
					while i := InStr(val, "\",, i+1)
					{
						if (SubStr(val, i+1, 1) != "u") ? (pos -= StrLen(SubStr(val, i)), next := "\") : 0
							continue 2
						
						; \uXXXX - JSON unicode escape sequence
						xxxx := Abs("0x" . SubStr(val, i+2, 4))
						if (A_IsUnicode || xxxx < 0x100)
							val := SubStr(val, 1, i-1) . Chr(xxxx) . SubStr(val, i+6)
					}
					
					if is_key
					{
						key := val, next := ":"
						continue
					}
				}
				
				else ; number | true | false | null
				{
					val := SubStr(src, pos, i := RegExMatch(src, "[\]\},\s]|$",, pos)-pos)
					
					; For numerical values, numerify integers and keep floats as is.
					; I'm not yet sure if I should numerify floats in v2.0-a ...
					static number := "number", integer := "integer"
					if val is %number%
					{
						if val is %integer%
							val += 0
					}
					; in v1.1, true,false,A_PtrSize,A_IsUnicode,A_Index,A_EventInfo,
					; SOMETIMES return strings due to certain optimizations. Since it
					; is just 'SOMETIMES', numerify to be consistent w/ v2.0-a
					else if (val == "true" || val == "false")
						val := %value% + 0
					; AHK_H has built-in null, can't do 'val := %value%' where value == "null"
					; as it would raise an exception in AHK_H(overriding built-in var)
					else if (val == "null")
						val := ""
					; any other values are invalid, continue to trigger error
					else if (pos--, next := "#")
						continue
					
					pos += i-1
				}
				
				is_array? ObjPush(obj, val) : obj[key] := val
				next := obj==tree ? "" : is_array ? ",]" : ",}"
			}
		}
		
		return tree[1]
	}
	
	Jxon_Dump(obj, indent:="", lvl:=1)
	{
		static q := Chr(34)
		
		if IsObject(obj)
		{
			static Type := Func("Type")
			if Type ? (Type.Call(obj) != "Object") : (ObjGetCapacity(obj) == "")
				throw Exception("Object type not supported.", -1, Format("<Object at 0x{:p}>", &obj))
			
			prefix := SubStr(A_ThisFunc, 1, InStr(A_ThisFunc, ".",, 0))
			fn_t := prefix "Jxon_True",  obj_t := this ? %fn_t%(this) : %fn_t%()
			fn_f := prefix "Jxon_False", obj_f := this ? %fn_f%(this) : %fn_f%()
			
			if (&obj == &obj_t)
				return "true"
			else if (&obj == &obj_f)
				return "false"
			
			is_array := 0
			for k in obj
				is_array := k == A_Index
			until !is_array
			
			static integer := "integer"
			if indent is %integer%
			{
				if (indent < 0)
					throw Exception("Indent parameter must be a postive integer.", -1, indent)
				spaces := indent, indent := ""
				Loop % spaces
					indent .= " "
			}
			indt := ""
			Loop, % indent ? lvl : 0
				indt .= indent
			
			this_fn := this ? Func(A_ThisFunc).Bind(this) : A_ThisFunc
			lvl += 1, out := "" ; Make #Warn happy
			for k, v in obj
			{
				if IsObject(k) || (k == "")
					throw Exception("Invalid object key.", -1, k ? Format("<Object at 0x{:p}>", &obj) : "<blank>")
				
				if !is_array
					out .= ( ObjGetCapacity([k], 1) ? %this_fn%(k) : q . k . q ) ;// key
				.  ( indent ? ": " : ":" ) ; token + padding
				out .= %this_fn%(v, indent, lvl) ; value
				.  ( indent ? ",`n" . indt : "," ) ; token + indent
			}
			
			if (out != "")
			{
				out := Trim(out, ",`n" . indent)
				if (indent != "")
					out := "`n" . indt . out . "`n" . SubStr(indt, StrLen(indent)+1)
			}
			
			return is_array ? "[" . out . "]" : "{" . out . "}"
		}
		
		; Number
		else if (ObjGetCapacity([obj], 1) == "")
			return obj
		
		; String (null -> not supported by AHK)
		if (obj != "")
		{
			obj := StrReplace(obj,  "\",    "\\")
			, obj := StrReplace(obj,  "/",    "\/")
			, obj := StrReplace(obj,    q, "\" . q)
			, obj := StrReplace(obj, "`b",    "\b")
			, obj := StrReplace(obj, "`f",    "\f")
			, obj := StrReplace(obj, "`n",    "\n")
			, obj := StrReplace(obj, "`r",    "\r")
			, obj := StrReplace(obj, "`t",    "\t")
			
			static needle := (A_AhkVersion<"2" ? "O)" : "") . "[^\x20-\x7e]"
			while RegExMatch(obj, needle, m)
				obj := StrReplace(obj, m[0], Format("\u{:04X}", Ord(m[0])))
		}
		
		return q . obj . q
	}
	
	Jxon_True()
	{
		static obj := {}
		return obj
	}
	
	Jxon_False()
	{
		static obj := {}
		return obj
	}
}
Thus, if set the page "Click informer", as well press the button "Get event data" then when the clicks will be called function MSGFunc(), getting the same text as div-tag.

Code: Select all

#NoEnv
#SingleInstance, Force
SetBatchLines, -1

#Include ../Chrome.ahk

; --- Create a new Chrome instance ---

addr := "https://autohotkey.com/boards/viewtopic.php?f=6&t=42890"
chPath := "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe"
profileName := "ChromeTest_Profile"
recycleProfile := false
showEvalResult := false

FileCreateDir, %profileName%
ChromeInst := new Chrome(profileName, addr,"--no-first-run", chPath,,"MSGFunc")

informerCode :=
	(LTrim Join`s
	"function handlerF(ev) {
		info.innerHTML = ``${ev.clientX}x ${ev.clientY}y``;
		console.info(``${ev.clientX}x ${ev.clientY}y``);
	}

	var body = document.querySelector('body');
	var info = document.createElement('div');
	info.id = 'info';
	info.style.position = 'fixed';
	info.style.top = '50px';
	info.style.right = '10px';
	info.style.width = '200px';
	info.style.border = '2px solid #fff';
	info.style.boxShadow = '1px 2px 2px #000';
	info.style.color = '#fff';
	info.style.fontWeight = 'bold';
	info.style.fontSize = '20px';
	info.style.textShadow = '1px 1px 1px #000, 1px -1px 1px #000, -1px -1px 1px #000, -1px 1px 1px #000';
	info.style.textAlign = 'center';
	body.appendChild(info);
	info.innerHTML = 'Click on the page';

	body.addEventListener('click', handlerF);"
	)
xmpl := "
(LTrim Join`r`n
	alert('Hello World!!!');
	/*
	var x = 5;
	alert(``x = ${x + 5}``);
	*/
)"
; --- Connect to the page ---

if !(PageInst := ChromeInst.GetPage()) {
	MsgBox, Could not retrieve page!
	ChromeInst.Kill()
	GoSub, CloseMe
} else {
	Gui,Input: Margin, 10, 10
	Gui,Input: +Hwndinput_h
	Gui,Input: Add, Button,gEvalMe,Run
	Gui,Input: Add, Text,x+10 yp+5,Run a JS-script on the page:
	Gui,Input: Font, s10 Bold, Consolas
	Gui,Input: Add, Edit,xm y+7 w600 r20 vmyCode cBlue,% xmpl
	Gui,Input: Font
	Gui,Input: Add, Button,xm y+5 gAddInform,Click inform
	Gui,Input: Add, Button,x+10 gCloseMe,Close
	Gui,Input: Add, Button,x+10 gTestMe,Get event data
	Gui,Input: Show,,Input
}
return
MSGFunc(msg) {
	ToolTip,% msg
}
TestMe:
	PageInst.Call("Console.enable")
return
EvalMe:
	Gui,Input: Submit, NoHide
	if (myCode == "")
		return
	GoSub, EvaluateHim
return
AddInform:
	myCode := informerCode
	GoSub, EvaluateHim
return
EvaluateHim:
	try
		Result := PageInst.Evaluate(myCode)
	catch e
	{
		MsgBox, % "Exception encountered in " e.What ":`n`n"
		. e.Message "`n`n"
		. "Specifically:`n`n"
		. Chrome.Jxon_Dump(Chrome.Jxon_Load(e.Extra), "`t")
	} if (showEvalResult)
		MsgBox, % "Result:`n" Chrome.Jxon_Dump(Result, "`t")
return
CloseMe:
InputGuiClose:

	; --- Close the Chrome instance ---
	
	try
		PageInst.Call("Browser.close") ; Fails when running headless
	catch
		ChromeInst.Kill()
	PageInst.Disconnect()
	
	fPath := Format("{1}\{2}",A_ScriptDir,profileName)
	if (recycleProfile) {
		FileRemoveDir,% fPath, 1
		i := 0
		while (i < 10, i++) {
			Sleep, 1000
			if (FileExist(fPath))
				FileRemoveDir,% fPath, 1
			else
				break
		}
	}
	ExitApp
May I ask you to add this opportunity to the future editions of your work? Having a this opportunity would be very convenient.

And sorry for my English. =/
geek
Posts: 1052
Joined: 02 Oct 2013, 22:13
Location: GeekDude
Contact:

Re: [Library] Chrome.ahk - Automate Google Chrome using native AutoHotkey. No Selenium!

21 Apr 2018, 22:38

KusochekDobra wrote:The only trouble-free solution that we managed to find is to activate the console domain:
PageInst.Call("Console.enable")
And catch incoming messages "Console.messageAdded"
I had to change the "Chrome" class a little, adding the sixth parameter to the call constructor, here and there to transfer it further for the nested class "Page", and rename it to "PageBase" and extend him, redefining the constructor and the method "Event"

...

May I ask you to add this opportunity to the future editions of your work? Having a this opportunity would be very convenient.

And sorry for my English. =/
This exactly the kind of thing I had in mind, but had not gotten around to implementing it. It's definitely on the to-do list to get this into the library, though rather than passing it into the constructor I might have it as a class property you would set (ChromeInst.OnConsole := Func("MyHandler") for example). I'll have to take a long look into it to make sure it's as future-proof and extensible as it needs to be. Who knows, I might incorporate your ideas directly :)

Your work is very good, and so is your English. Thank you for sharing with us!

And now for some hype

Image
KusochekDobra
Posts: 38
Joined: 25 Apr 2016, 18:00

Re: [Library] Chrome.ahk - Automate Google Chrome using native AutoHotkey. No Selenium!

22 Apr 2018, 04:53

Oh, it's so great!!!

In a large family AHK it is customary to be generous to knowledge. It's nice to be a part of this!
Look forward to continuing! :thumbup:
User avatar
yawikflame
Posts: 21
Joined: 02 Jan 2017, 06:19
Contact:

Re: [Library] Chrome.ahk - Automate Google Chrome using native AutoHotkey. No Selenium!

24 Apr 2018, 15:09

Awesome stuff GeekDude!
small question,
is there a way to retrieve the source code of the current working tab?
thank you so much for this great code!!
cheers ;)
yawik
User avatar
Xtra
Posts: 2744
Joined: 02 Oct 2015, 12:15

Re: [Library] Chrome.ahk - Automate Google Chrome using native AutoHotkey. No Selenium!

24 Apr 2018, 17:21

yawikflame wrote:is there a way to retrieve the source code of the current working tab?
PageInst.Evaluate("document.getElementsByTagName('html')[0].textContent;").Value
KusochekDobra
Posts: 38
Joined: 25 Apr 2016, 18:00

Re: [Library] Chrome.ahk - Automate Google Chrome using native AutoHotkey. No Selenium!

24 Apr 2018, 19:25

If under the control of the script will be several pages and all of them will be send a messages, then we need implement the concept of the event stack for make a manage all of them without transmittance. For example:

Code: Select all

s := New Stacker()
Esc::
	ExitApp
Class Stacker {
	__New() {
		this.eventStack := []
		modify			:= "^"				; Ctrl
		key 			:= "LButton"		; LMouseButton
		forState		:= "Up"				; For release
		this.mPos 		:= ""
		%key%%forState% := ObjBindMethod(this, "_InsertEvent")
		this.ePop		:= ObjBindMethod(this, "_PopEvent")
		Hotkey,% Format("~*{1}{2} {3}",modify,key,forState),% %key%%forState%
	}
	_InsertEvent() {
		MouseGetPos, xx, yy
		this.eventStack.InsertAt(1, [xx, yy])
		ePop := this.ePop
		SetTimer,% ePop, -10
	}
	_PopEvent() {
		while (this.eventStack.Length()) {
			fifo := this.eventStack.Pop()
			MsgBox,,Title,% Format("x = '{1}' <|> y = '{2}'", fifo[1], fifo[2])
		}
	}
}
While MsgBox is showed, we can more press Ctrl+LButton and all other coordinates of Click, will be showed later.
I suggest adding event processing by default in Class Page and add external simple class EventStacker, who will do this:

Code: Select all

Class EventStacker {
	__New() {
		this.eventStack := []
		this.ePop		:= ObjBindMethod(this, "PopEvent")
	}
	InsertEvent(msg) {
		this.eventStack.InsertAt(1, msg)
		ePop := this.ePop
		SetTimer,% ePop, -10
	}
	PopEvent() {
		while (this.eventStack.Length()) {
			fifo := this.eventStack.Pop()
			MsgBox,,Title,%	fifo
		}
	}
}
This will be convenient for the developer in that it will not interfere in the code of Class Chrome. For change main behavior, it will only be necessary to make changes to his method PopEvent(), and the Class EventStacker itself can be easily put in the body of the main script, for easy editing. Then, you just need to make a couple of simple changes, first, in the constructor Class Page, for example:

Code: Select all

this.e := New EventStacker()
And the second, in the condition else if (EventName == "Message") of the method Event(), of the same class. For example:

Code: Select all

else if (EventName == "Message")
{
	data := Chrome.Jxon_Load(Event.data)
	if (data.method == "Console.messageAdded" && (msg := data.params.message).level == "info")
		this.e.InsertEvent(msg.text)
	if this.responses.HasKey(data.ID)
		this.responses[data.ID] := data
}
Thus, by default, if the console has been enabled, all events from the browser console at the level "info", will be added at event stack.

It would be great to see a something similar. :)

Edit:
More precisely not a stack, but a queue. :)

Return to “Scripts and Functions (v1)”

Who is online

Users browsing this forum: charlie89, gwarble, Spikea and 130 guests