├── .gitattributes ├── .gitignore ├── CHANGELOG.md ├── Gopkg.lock ├── Gopkg.toml ├── LICENSE ├── README.md ├── api_overrides.go ├── autogcd.go ├── autogcd_test.go ├── conditionals.go ├── element.go ├── element_test.go ├── examples └── googler │ └── googler.go ├── settings.go ├── tab.go ├── tab_subscribers.go ├── tab_test.go ├── testdata ├── attributes.html ├── background.html ├── big_body.html ├── button.html ├── console_log.html ├── cookie1.html ├── cookie2.html ├── dblclick.html ├── events.html ├── frame_a.html ├── frame_b.html ├── frame_top.html ├── frameset.html ├── iframe.html ├── index.html ├── inner.html ├── input.html ├── invalidated.html ├── mouseover.html ├── prompt.html ├── redirect.html ├── redirect_target.html ├── script.html ├── script_inner.html ├── scroll.html ├── table.html ├── window_main.html ├── window_sub1.html └── window_sub2.html ├── types.go └── vendor ├── github.com └── wirepair │ └── gcd │ ├── .gitattributes │ ├── .gitignore │ ├── CHANGELOG.md │ ├── Gopkg.lock │ ├── Gopkg.toml │ ├── LICENSE │ ├── README.md │ ├── chrome_target.go │ ├── gcd.go │ ├── gcdapi │ ├── README.md │ ├── accessibility.go │ ├── animation.go │ ├── applicationcache.go │ ├── audits.go │ ├── browser.go │ ├── cachestorage.go │ ├── console.go │ ├── css.go │ ├── database.go │ ├── debugger.go │ ├── deviceorientation.go │ ├── dom.go │ ├── domdebugger.go │ ├── domsnapshot.go │ ├── domstorage.go │ ├── emulation.go │ ├── fetch.go │ ├── headlessexperimental.go │ ├── heapprofiler.go │ ├── indexeddb.go │ ├── input.go │ ├── inspector.go │ ├── io.go │ ├── layertree.go │ ├── log.go │ ├── memory.go │ ├── network.go │ ├── overlay.go │ ├── page.go │ ├── performance.go │ ├── profiler.go │ ├── runtime.go │ ├── schema.go │ ├── security.go │ ├── serviceworker.go │ ├── storage.go │ ├── systeminfo.go │ ├── target.go │ ├── testing.go │ ├── tethering.go │ ├── tracing.go │ └── version.go │ └── gcdmessage │ └── gcdmessage.go └── golang.org └── x └── net ├── AUTHORS ├── CONTRIBUTORS ├── LICENSE ├── PATENTS └── websocket ├── client.go ├── dial.go ├── hybi.go ├── server.go └── websocket.go /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # Windows shortcuts 18 | *.lnk 19 | 20 | # ========================= 21 | # Operating System Files 22 | # ========================= 23 | 24 | # OSX 25 | # ========================= 26 | 27 | .DS_Store 28 | .AppleDouble 29 | .LSOverride 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear on external disk 35 | .Spotlight-V100 36 | .Trashes 37 | 38 | # Directories potentially created on remote AFP share 39 | .AppleDB 40 | .AppleDesktop 41 | Network Trash Folder 42 | Temporary Items 43 | .apdisk 44 | debug.log 45 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Changelog (2018) 4 | - December 8th: Update to latest gcd / protocol.json file for 71.0.3578.80 5 | - April 24th: Updated to latest gcd / protocol.json file for 66.0.3359.117 for *stable* branch. 6 | - Feburary 22nd: Updated to latest gcd / protocol.json file for 66.0.3346.8. Added dep init/dep ensure to repository for package management. 7 | - January 17th: Updated to latest gcd / protocol.json file for 65.0.3322.3. 8 | 9 | ## Changelog (2017) 10 | - November 20th: Updated to latest gcd / protocol.json file for 64.0.3269.3. Navigate now exposes friendly error text. 11 | - October 30th: Updated to latest gcd / protocol.json file for 64.0.3251.0. 12 | - September 9th: Updated to latest gcd / protocol.json file for 62.0.3202.9. We are now bound to the browser version (latest dev channel). See [gcd](https://github.com/wirepair/gcd/) updates for more information. Note if you change the gcd channel, you'll probably end up breaking a lot of methods in autogcd, so be warned. 13 | - August 15th: Updated to the latest gcd / protocol.json file. 14 | - May: Updated to latest gcd / protocol.json file. 15 | - April: Updated to latest gcd / protocol.json file. Fixed unit tests to wait for stability a bit longer as node changes seem to take longer than before. -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | branch = "master" 6 | name = "github.com/wirepair/gcd" 7 | packages = [ 8 | ".", 9 | "gcdapi", 10 | "gcdmessage" 11 | ] 12 | revision = "6ed90b36a93267191f45318344c02c34bf672144" 13 | 14 | [[projects]] 15 | branch = "master" 16 | name = "golang.org/x/net" 17 | packages = ["websocket"] 18 | revision = "610586996380ceef02dd726cc09df7e00a3f8e56" 19 | 20 | [solve-meta] 21 | analyzer-name = "dep" 22 | analyzer-version = 1 23 | inputs-digest = "a1a6bf0b7074b682aa653392bf970f4203ed59ebfd2798073d6fb0ae1524e80c" 24 | solver-name = "gps-cdcl" 25 | solver-version = 1 26 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | # Gopkg.toml example 2 | # 3 | # Refer to https://golang.github.io/dep/docs/Gopkg.toml.html 4 | # for detailed Gopkg.toml documentation. 5 | # 6 | # required = ["github.com/user/thing/cmd/thing"] 7 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 8 | # 9 | # [[constraint]] 10 | # name = "github.com/user/project" 11 | # version = "1.0.0" 12 | # 13 | # [[constraint]] 14 | # name = "github.com/user/project2" 15 | # branch = "dev" 16 | # source = "github.com/myfork/project2" 17 | # 18 | # [[override]] 19 | # name = "github.com/x/y" 20 | # version = "2.4.0" 21 | # 22 | # [prune] 23 | # non-go = false 24 | # go-tests = true 25 | # unused-packages = true 26 | 27 | 28 | [[constraint]] 29 | branch = "master" 30 | name = "github.com/wirepair/gcd" 31 | 32 | [prune] 33 | go-tests = true 34 | unused-packages = true 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 isaac dawson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Automating Google Chrome Debugger (autogcd) 2 | Autogcd is a wrapper around the [gcd](https://github.com/wirepair/gcd/) library to enable automation of Google Chrome. It some what mimics the functionality offered by WebDriver but allows more low level access via the debugger service. 3 | 4 | # Changelog 5 | See the [CHANGELOG](https://github.com/wirepair/autogcd/tree/master/CHANGELOG.md). 6 | 7 | ## Dependencies 8 | autogcd requires [gcd](https://github.com/wirepair/gcd/), [gcdapi](https://github.com/wirepair/gcd/tree/master/gcdapi) and [gcdmessage](https://github.com/wirepair/gcd/tree/master/gcdmessage) packages. 9 | 10 | ## The API 11 | Autogcd is comprised of four components: 12 | * [autogcd.go](https://github.com/wirepair/autogcd/tree/master/autogcd.go) - The wrapper around gcd.Gcd. 13 | * [settings.go](https://github.com/wirepair/autogcd/tree/master/settings.go) - For managing startup of autogcd. 14 | * [tab.go](https://github.com/wirepair/autogcd/tree/master/tab.go) - Individual chrome tabs 15 | * [element.go](https://github.com/wirepair/autogcd/tree/master/element.go) - Elements that make up the page (includes iframes, #documents as well) 16 | 17 | ## API Documentation 18 | [Documentation](https://godoc.org/github.com/wirepair/autogcd/) 19 | 20 | ## Usage 21 | See the [examples](https://github.com/wirepair/autogcd/tree/master/examples) or the various testcases. 22 | 23 | ## Notes 24 | The chrome debugger service uses internal nodeIds for identifying unique elements/nodes in the DOM. In most cases you will not need to use this identifier directly, however if you plan on calling gcdapi related features you will probably need it. The most common example of when you'll need them is for getting access to a nested #document element inside of an iframe. To run query selectors on nested documents, the nodeId of the iframe #document must be known. 25 | 26 | ### Elements 27 | The Chrome Debugger by nature is far more asynchronous than WebDriver. It is possible to work with elements even though the debugger has not yet notified us of their existence. To deal with this, Elements can be in multiple states; Ready, NotReady or Invalid. Only certain features are available when an Element is in a Ready state. If an Element is Invalid, it should no longer be used and references to it should be discarded. 28 | 29 | ### Frames 30 | If you need to search elements (by id or by a selector) of a frame's #document, you'll need to get an Element reference that is the iframe's #document. This can be done by doing a tab.GetElementsBySelector("iframe"), iterating over the results and calling element.GetFrameDocumentNodeId(). This will return the internal document node id which you can then pass to tab.GetDocumentElementsBySelector(iframeDocNodeId, "#whatever"). 31 | 32 | ### Windows 33 | The major limitation of using the Google Chrome Remote Debugger is when working with windows. Since each tab must have the debugger enabled, calls to window.open will open a new window prior to us being able to attach a debugger. To get around this, you'll need to get a list of tabs AutoGcd.GetAllTabs(), then call AutoGcd.RefreshTabList() which will connect each tab to an autogcd.Tab. You'd then need to reload the tab get begin working with it. 34 | 35 | ### Stability & Waiting 36 | There are a few ways you can test for stability or if an Element is ready. Element.WaitForReady() will not return until the debugger service has populated the element's information. If you are waiting for a page to stabilize, you can use the tab.WaitStable() method which won't return until it hasn't seen any DOM nodes being added/removed for a configurable (tab.SetStabilityTime(...)) amount of time. 37 | 38 | Finally, you can use the tab.WaitFor method, which takes a ConditionalFunc type and repeatedly calls it until it returns true, or times out. 39 | 40 | For example/simple ConditionalFuncs see the [conditionals.go](https://github.com/wirepair/autogcd/tree/master/conditionals.go) source. Of course you can use whatever you want as long as it matches the ConditionalFunc signature. 41 | 42 | ### Navigation Errors 43 | Unlike WebDriver, we can determine if navigation fails, *at least in chromium. After tab.Navigate(url), calling tab.DidNavigationFail() will return a true/false return value along with a string of the failure type if one did occur. It is strongly recommended you pass the following flags: --test-type, --ignore-certificate-errors on start up of autogcd if you wish to ignore certificate errors. 44 | 45 | \* This does not appear to work in chrome in windows or osx. 46 | 47 | ### Input 48 | Only a limited set of input functions have been implemented. Clicking and sending keys. You can use Element.SendKeys() or send the keys to whatever is focused by using Tab.SendKeys(). Only Enter ("\n"), Tab ("\t") and Backspace ("\b") were implemented, to use them, simply add them to your SendKeys argument Element.SendKeys("enter text hit enter\n") where \n will cause the enter key to be pressed. 49 | 50 | ### Listeners 51 | Four listener functions have been implemented, GetConsoleMessages, GetNetworkTraffic, GetStorageEvents, GetDOMChanges. 52 | 53 | #### GetConsoleMessages 54 | Pass in a ConsoleMessageFunc handler to begin receiving console messages from the tab. Use StopConsoleMessages to stop receiving them. 55 | 56 | #### GetNetworkTraffic 57 | Pass in either a NetworkRequestHandlerFunc, NetworkResponseHandlerFunc or NetworkFinishedHandlerFunc handler (or all three) to receive network traffic events. NetworkFinishedHandler should be used to signal your application that it's safe to get the response body of the request. While calling GetResponseBody *may* work from NetworkResponseHandlerFunc, it will in many cases fail as the debugger service isn't ready to return the data yet. Use StopNetworkTraffic to stop receiving them. 58 | 59 | #### GetStorageEvents 60 | Pass in a StorageFunc handler to recieve cleared, removed, added and updated storage events. Use StopStorageEvents to stop receiving them. 61 | 62 | #### GetDOMChanges 63 | Pass in a DomChangeHandlerFunc to receive various dom change events. Call it with a nil handler to stop receiving them. 64 | 65 | ## Calling gcd directly 66 | AutoGcd has not implemented all of the Google Chrome Debugger protocol methods and features because I don't see any point in wrapping a lot of them. However, you are not out of luck, all gcd components are bound to each Tab object. I'd suggest reviewing the gcdapi package if there is a particular component you wish to use. All components are bound to the Tab so it should be as simple as calling Tab.{component}.{method}. 67 | 68 | ### Overriding gcd 69 | Take a look at [api_overrides.go](https://github.com/wirepair/autogcd/tree/master/api_overrides.go) for an example of overriding gcd methods. In 70 | some cases the protocol.json specification is incorrect, in which case you may need to override specific methods. Since I designed the packages 71 | to use an intermediary gcdmessage package for requests and responses you're completely free to override anything necessary. 72 | 73 | ## Internals 74 | I'll admit, I do not fully like the design of the Elements. I have to track state updates very carefully and I chose to use sync.RWMutex locks. I couldn't see an obvious method of using channels to synchronize access to the DOMNodes. I'm very open to new architectures/designs if someone has a better method of keeping Element objects up to date as Chrome notifies autogcd of new values. 75 | 76 | As mentioned in the Elements section, Chrome Debugger Protocol is fully asynchronous. The debugger is only notified of elements when the page first loads (and even then only a few of the top level elements). It also occurs when an element has been modified, or when you request them with DOM.requestChildNodes. Autogcd tries to manage all of this for you, but there may be a case where you search for elements that chrome has not notified the debugger client yet. In this case the Element will be, in autogcd terminology, NotReady. This means you can sort of work with it because we know its nodeId but we may not know much else (even what type of node it is). Internally almost all chrome debugger methods take nodeIds. 77 | 78 | This package has been *heavily* tested in the real world. It was used to scan the top 1 million websites from Alexa. I found numerous goroutine leaks that have been subsequently fixed. After running my scan I no longer see any leaks. It should also be completely safe to kill the browser at any point and not have any runaway go routines since I have channels waiting for close messages at any point a channel is sending or receiving. 79 | 80 | ## Reporting Bugs & Requesting Features 81 | Found a bug? Great! Tell me what version of chrome/chromium you are using and how to reproduce and I'll get to it when I can. Keep in mind this is a side project for me. Same goes for new features. Patches obviously welcome. 82 | 83 | 84 | ## License 85 | The MIT License (MIT) 86 | 87 | Copyright (c) 2016 isaac dawson 88 | 89 | Permission is hereby granted, free of charge, to any person obtaining a copy 90 | of this software and associated documentation files (the "Software"), to deal 91 | in the Software without restriction, including without limitation the rights 92 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 93 | copies of the Software, and to permit persons to whom the Software is 94 | furnished to do so, subject to the following conditions: 95 | 96 | The above copyright notice and this permission notice shall be included in 97 | all copies or substantial portions of the Software. 98 | 99 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 100 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 101 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 102 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 103 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 104 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 105 | THE SOFTWARE. -------------------------------------------------------------------------------- /api_overrides.go: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2017 isaac dawson 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | */ 24 | 25 | package autogcd 26 | 27 | import ( 28 | "encoding/json" 29 | "github.com/wirepair/gcd" 30 | "github.com/wirepair/gcd/gcdapi" 31 | "github.com/wirepair/gcd/gcdmessage" 32 | ) 33 | 34 | /* 35 | This is for overriding gcdapi calls that for some reason or another need parameters removed or they won't 36 | work. 37 | */ 38 | 39 | // Evaluate - Evaluates expression on global object. 40 | // expression - Expression to evaluate. 41 | // objectGroup - Symbolic group name that can be used to release multiple objects. 42 | // includeCommandLineAPI - Determines whether Command Line API should be available during the evaluation. 43 | // silent - In silent mode exceptions thrown during evaluation are not reported and do not pause execution. Overrides setPauseOnException state. 44 | // contextId - Specifies in which execution context to perform evaluation. If the parameter is omitted the evaluation will be performed in the context of the inspected page. 45 | // returnByValue - Whether the result is expected to be a JSON object that should be sent by value. 46 | // generatePreview - Whether preview should be generated for the result. 47 | // userGesture - Whether execution should be treated as initiated by user in the UI. 48 | // awaitPromise - Whether execution should wait for promise to be resolved. If the result of evaluation is not a Promise, it's considered to be an error. 49 | // Returns - result - Evaluation result. exceptionDetails - Exception details. 50 | func overridenRuntimeEvaluate(target *gcd.ChromeTarget, expression string, objectGroup string, includeCommandLineAPI bool, silent bool, contextId int, returnByValue bool, generatePreview bool, userGesture bool, awaitPromise bool) (*gcdapi.RuntimeRemoteObject, *gcdapi.RuntimeExceptionDetails, error) { 51 | paramRequest := make(map[string]interface{}, 9) 52 | paramRequest["expression"] = expression 53 | paramRequest["objectGroup"] = objectGroup 54 | paramRequest["includeCommandLineAPI"] = includeCommandLineAPI 55 | paramRequest["silent"] = silent 56 | // only add context id if it's non-zero 57 | if contextId != 0 { 58 | paramRequest["contextId"] = contextId 59 | } 60 | paramRequest["returnByValue"] = returnByValue 61 | paramRequest["generatePreview"] = generatePreview 62 | paramRequest["userGesture"] = userGesture 63 | paramRequest["awaitPromise"] = awaitPromise 64 | resp, err := gcdmessage.SendCustomReturn(target, target.GetSendCh(), &gcdmessage.ParamRequest{Id: target.GetId(), Method: "Runtime.evaluate", Params: paramRequest}) 65 | if err != nil { 66 | return nil, nil, err 67 | } 68 | 69 | var chromeData struct { 70 | Result struct { 71 | Result *gcdapi.RuntimeRemoteObject 72 | ExceptionDetails *gcdapi.RuntimeExceptionDetails 73 | } 74 | } 75 | 76 | if resp == nil { 77 | return nil, nil, &gcdmessage.ChromeEmptyResponseErr{} 78 | } 79 | 80 | // test if error first 81 | cerr := &gcdmessage.ChromeErrorResponse{} 82 | json.Unmarshal(resp.Data, cerr) 83 | if cerr != nil && cerr.Error != nil { 84 | return nil, nil, &gcdmessage.ChromeRequestErr{Resp: cerr} 85 | } 86 | 87 | if err := json.Unmarshal(resp.Data, &chromeData); err != nil { 88 | return nil, nil, err 89 | } 90 | 91 | return chromeData.Result.Result, chromeData.Result.ExceptionDetails, nil 92 | } 93 | -------------------------------------------------------------------------------- /autogcd.go: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2017 isaac dawson 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | */ 24 | 25 | /* 26 | Autogcd - An automation interface for https://github.com/wirepair/gcd. Contains most functionality 27 | found in WebDriver and extends it to offer more low level features. This library was built due to 28 | WebDriver/Chromedriver also using the debugger service. Since it is not possible to attach to a Page's 29 | debugger twice, automating a custom extension with WebDriver turned out to not be possible. 30 | 31 | The Chrome Debugger by nature is far more asynchronous than WebDriver. It is possible to work with 32 | elements even though the debugger has not yet notified us of their existence. To deal with this, Elements 33 | can be in multiple states; Ready, NotReady or Invalid. Only certain features are available when an Element 34 | is in a Ready state. If an Element is Invalid, it should no longer be used and references to it should be 35 | discarded. 36 | 37 | Dealing with frames is also different than WebDriver. There is no SwitchToFrame, you simply pass in the frameId 38 | to certain methods that require it. You can lookup the these frame documents by finding frame/iframe Elements and 39 | requesting the document NodeId reference via the GetFrameDocumentNodeId method. 40 | 41 | Lastly, dealing with windows... doesn't really work since they open a new tab. A possible solution would be to monitor 42 | the list of tabs by calling AutoGcd.RefreshTabs() and doing a diff of known versus new. You could then do a Tab.Reload() 43 | to refresh the page. It is recommended that you clear cache on the tab first so it is possible to trap the various 44 | network events. There are other dirty hacks you could do as well, such as injecting script to override window.open, 45 | or rewriting links etc. 46 | */ 47 | package autogcd 48 | 49 | import ( 50 | "errors" 51 | "fmt" 52 | "os" 53 | "sync" 54 | 55 | "github.com/wirepair/gcd" 56 | ) 57 | 58 | type AutoGcd struct { 59 | debugger *gcd.Gcd 60 | settings *Settings 61 | tabLock *sync.RWMutex 62 | tabs map[string]*Tab 63 | shutdown bool 64 | } 65 | 66 | // Creates a new AutoGcd based off the provided settings. 67 | func NewAutoGcd(settings *Settings) *AutoGcd { 68 | auto := &AutoGcd{settings: settings} 69 | auto.tabLock = &sync.RWMutex{} 70 | auto.tabs = make(map[string]*Tab) 71 | auto.debugger = gcd.NewChromeDebugger() 72 | auto.debugger.SetTerminationHandler(auto.defaultTerminationHandler) 73 | if len(settings.extensions) > 0 { 74 | auto.debugger.AddFlags(settings.extensions) 75 | } 76 | 77 | if len(settings.flags) > 0 { 78 | auto.debugger.AddFlags(settings.flags) 79 | } 80 | 81 | if settings.timeout > 0 { 82 | auto.debugger.SetTimeout(settings.timeout) 83 | } 84 | 85 | if len(settings.env) > 0 { 86 | auto.debugger.AddEnvironmentVars(settings.env) 87 | } 88 | return auto 89 | } 90 | 91 | // Default termination handling is to log, override with SetTerminationHandler 92 | func (auto *AutoGcd) defaultTerminationHandler(reason string) { 93 | fmt.Printf("chrome was terminated: %s\n", reason) 94 | } 95 | 96 | // Allow callers to handle chrome terminating. 97 | func (auto *AutoGcd) SetTerminationHandler(handler gcd.TerminatedHandler) { 98 | auto.debugger.SetTerminationHandler(handler) 99 | } 100 | 101 | // Starts Google Chrome with debugging enabled. 102 | func (auto *AutoGcd) Start() error { 103 | if auto.settings.connectToInstance { 104 | auto.debugger.ConnectToInstance(auto.settings.chromeHost, auto.settings.chromePort) 105 | } else { 106 | auto.debugger.StartProcess(auto.settings.chromePath, auto.settings.userDir, auto.settings.chromePort) 107 | } 108 | 109 | tabs, err := auto.debugger.GetTargets() 110 | if err != nil { 111 | return err 112 | } 113 | auto.tabLock.Lock() 114 | for _, tab := range tabs { 115 | t, err := open(tab) 116 | if err != nil { 117 | return err 118 | } 119 | auto.tabs[tab.Target.Id] = t 120 | } 121 | auto.tabLock.Unlock() 122 | return nil 123 | } 124 | 125 | // Closes all tabs and shuts down the browser. 126 | func (auto *AutoGcd) Shutdown() error { 127 | if auto.shutdown { 128 | return errors.New("AutoGcd already shut down.") 129 | } 130 | 131 | auto.tabLock.Lock() 132 | for _, tab := range auto.tabs { 133 | tab.close() // exit go routines 134 | auto.debugger.CloseTab(tab.ChromeTarget) 135 | 136 | } 137 | auto.tabLock.Unlock() 138 | 139 | if !auto.settings.connectToInstance { 140 | err := auto.debugger.ExitProcess() 141 | if auto.settings.removeUserDir == true { 142 | return os.RemoveAll(auto.settings.userDir) 143 | } 144 | return err 145 | } 146 | 147 | auto.shutdown = true 148 | return nil 149 | } 150 | 151 | // Refreshs our internal list of tabs and return all tabs 152 | func (auto *AutoGcd) RefreshTabList() (map[string]*Tab, error) { 153 | 154 | knownTabs := auto.GetAllTabs() 155 | knownIds := make(map[string]struct{}, len(knownTabs)) 156 | for _, v := range knownTabs { 157 | knownIds[v.Target.Id] = struct{}{} 158 | } 159 | newTabs, err := auto.debugger.GetNewTargets(knownIds) 160 | if err != nil { 161 | return nil, err 162 | } 163 | 164 | auto.tabLock.Lock() 165 | for _, newTab := range newTabs { 166 | t, err := open(newTab) 167 | if err != nil { 168 | return nil, err 169 | } 170 | auto.tabs[newTab.Target.Id] = t 171 | } 172 | auto.tabLock.Unlock() 173 | return auto.GetAllTabs(), nil 174 | } 175 | 176 | // Returns the first "visual" tab. 177 | func (auto *AutoGcd) GetTab() (*Tab, error) { 178 | auto.tabLock.RLock() 179 | defer auto.tabLock.RUnlock() 180 | for _, tab := range auto.tabs { 181 | if tab.Target.Type == "page" { 182 | return tab, nil 183 | } 184 | } 185 | return nil, &InvalidTabErr{Message: "no Page tab types found"} 186 | } 187 | 188 | // Returns a safe copy of tabs 189 | func (auto *AutoGcd) GetAllTabs() map[string]*Tab { 190 | auto.tabLock.RLock() 191 | defer auto.tabLock.RUnlock() 192 | tabs := make(map[string]*Tab) 193 | for id, tab := range auto.tabs { 194 | tabs[id] = tab 195 | } 196 | return tabs 197 | } 198 | 199 | // Activate the tab in the chrome UI 200 | func (auto *AutoGcd) ActivateTab(tab *Tab) error { 201 | return auto.debugger.ActivateTab(tab.ChromeTarget) 202 | } 203 | 204 | // Activate the tab in the chrome UI, by tab id 205 | func (auto *AutoGcd) ActivateTabById(id string) error { 206 | tab, err := auto.tabById(id) 207 | if err != nil { 208 | return err 209 | } 210 | return auto.ActivateTab(tab) 211 | } 212 | 213 | // Creates a new tab 214 | func (auto *AutoGcd) NewTab() (*Tab, error) { 215 | target, err := auto.debugger.NewTab() 216 | if err != nil { 217 | return nil, &InvalidTabErr{Message: "unable to create tab: " + err.Error()} 218 | } 219 | auto.tabLock.Lock() 220 | defer auto.tabLock.Unlock() 221 | 222 | tab, err := open(target) 223 | if err != nil { 224 | return nil, err 225 | } 226 | auto.tabs[target.Target.Id] = tab 227 | return tab, nil 228 | } 229 | 230 | // Closes the provided tab. 231 | func (auto *AutoGcd) CloseTab(tab *Tab) error { 232 | tab.close() // kill listening go routines 233 | 234 | if err := auto.debugger.CloseTab(tab.ChromeTarget); err != nil { 235 | return err 236 | } 237 | 238 | auto.tabLock.Lock() 239 | defer auto.tabLock.Unlock() 240 | 241 | delete(auto.tabs, tab.Target.Id) 242 | return nil 243 | } 244 | 245 | // Closes a tab based off the tab id. 246 | func (auto *AutoGcd) CloseTabById(id string) error { 247 | tab, err := auto.tabById(id) 248 | if err != nil { 249 | return err 250 | } 251 | auto.CloseTab(tab) 252 | return nil 253 | } 254 | 255 | // Finds the tab by its id. 256 | func (auto *AutoGcd) tabById(id string) (*Tab, error) { 257 | auto.tabLock.RLock() 258 | tab := auto.tabs[id] 259 | auto.tabLock.RUnlock() 260 | if tab == nil { 261 | return nil, &InvalidTabErr{"unknown tab id " + id} 262 | } 263 | return tab, nil 264 | } 265 | 266 | func (auto *AutoGcd) GetChromeRevision() string { 267 | return auto.debugger.GetRevision() 268 | } 269 | -------------------------------------------------------------------------------- /autogcd_test.go: -------------------------------------------------------------------------------- 1 | package autogcd 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "net" 9 | "net/http" 10 | "os" 11 | "os/exec" 12 | "runtime" 13 | "strings" 14 | "testing" 15 | "time" 16 | ) 17 | 18 | var ( 19 | testListener net.Listener 20 | testPath string 21 | testDir string 22 | testPort string 23 | testServerAddr string 24 | ) 25 | 26 | var testStartupFlags = []string{"--test-type", "--ignore-certificate-errors", "--allow-running-insecure-content", "--disable-new-tab-first-run", "--no-first-run", "--disable-translate", "--safebrowsing-disable-auto-update", "--disable-component-update", "--safebrowsing-disable-download-protection"} 27 | 28 | func init() { 29 | switch runtime.GOOS { 30 | case "windows": 31 | flag.StringVar(&testPath, "chrome", "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe", "path to chrome") 32 | flag.StringVar(&testDir, "dir", "C:\\temp\\", "user directory") 33 | case "darwin": 34 | flag.StringVar(&testPath, "chrome", "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary", "path to chrome") 35 | flag.StringVar(&testDir, "dir", "/tmp/", "user directory") 36 | case "linux": 37 | flag.StringVar(&testPath, "chrome", "/usr/bin/chromium-browser", "path to chrome") 38 | flag.StringVar(&testDir, "dir", "/tmp/", "user directory") 39 | } 40 | flag.StringVar(&testPort, "port", "9222", "Debugger port") 41 | } 42 | 43 | func TestMain(m *testing.M) { 44 | flag.Parse() 45 | testServer() 46 | ret := m.Run() 47 | testCleanUp() 48 | os.Exit(ret) 49 | } 50 | 51 | func testCleanUp() { 52 | testListener.Close() 53 | } 54 | 55 | func TestStart(t *testing.T) { 56 | s := NewSettings(testPath, testRandomDir(t)) 57 | s.SetStartTimeout(15) 58 | s.SetChromeHost("localhost") 59 | auto := NewAutoGcd(s) 60 | defer auto.Shutdown() 61 | 62 | if err := auto.Start(); err != nil { 63 | t.Fatalf("failed to start chrome: %s\n", err) 64 | } 65 | auto.SetTerminationHandler(nil) 66 | } 67 | 68 | func TestGetTabCheckVersion(t *testing.T) { 69 | var err error 70 | var tab *Tab 71 | var product string 72 | 73 | auto := testDefaultStartup(t) 74 | defer auto.Shutdown() 75 | 76 | tab, err = auto.GetTab() 77 | if err != nil { 78 | t.Fatalf("Error getting tab: %s\n", err) 79 | } 80 | 81 | if tab.Target.Type != "page" { 82 | t.Fatalf("Got tab but wasn't of type Page") 83 | } 84 | 85 | if _, product, _, _, _, err = tab.Browser.GetVersion(); err != nil { 86 | t.Fatalf("Error getting browser version information: %s\n", err) 87 | } 88 | 89 | if !strings.Contains(product, auto.GetChromeRevision()) { 90 | t.Fatalf("Error browser version: %s does not match gcd api version: %s\n", product, auto.GetChromeRevision()) 91 | 92 | } 93 | } 94 | 95 | func TestNewTab(t *testing.T) { 96 | //var newTab *Tab 97 | auto := testDefaultStartup(t) 98 | defer auto.Shutdown() 99 | 100 | tabLen := len(auto.tabs) 101 | _, err := auto.NewTab() 102 | if err != nil { 103 | t.Fatalf("error creating new tab: %s\n", err) 104 | } 105 | 106 | if tabLen+1 != len(auto.tabs) { 107 | t.Fatalf("error created new tab but not reflected in our map") 108 | } 109 | } 110 | 111 | func TestCloseTab(t *testing.T) { 112 | var err error 113 | var newTab *Tab 114 | auto := testDefaultStartup(t) 115 | defer auto.Shutdown() 116 | 117 | tabLen := len(auto.tabs) 118 | 119 | newTab, err = auto.NewTab() 120 | if err != nil { 121 | t.Fatalf("error creating new tab: %s\n", err) 122 | } 123 | 124 | if tabLen+1 != len(auto.tabs) { 125 | t.Fatalf("error created new tab but not reflected in our map") 126 | } 127 | 128 | err = auto.CloseTab(newTab) 129 | if err != nil { 130 | t.Fatalf("error closing tab") 131 | } 132 | 133 | if _, err := auto.tabById(newTab.Target.Id); err == nil { 134 | t.Fatalf("error closed tab still in our map") 135 | } 136 | } 137 | 138 | func TestChromeTermination(t *testing.T) { 139 | auto := testDefaultStartup(t) 140 | doneCh := make(chan struct{}) 141 | shutdown := time.NewTimer(time.Second * 4) 142 | timeout := time.NewTimer(time.Second * 10) 143 | terminatedHandler := func(reason string) { 144 | t.Logf("reason: %s\n", reason) 145 | doneCh <- struct{}{} 146 | } 147 | 148 | auto.SetTerminationHandler(terminatedHandler) 149 | for { 150 | select { 151 | case <-doneCh: 152 | goto DONE 153 | case <-shutdown.C: 154 | auto.Shutdown() 155 | case <-timeout.C: 156 | t.Fatalf("timed out waiting for termination") 157 | } 158 | } 159 | DONE: 160 | } 161 | 162 | func testDefaultStartup(t *testing.T) *AutoGcd { 163 | s := NewSettings(testPath, testRandomDir(t)) 164 | s.RemoveUserDir(true) 165 | s.AddStartupFlags(testStartupFlags) 166 | s.SetDebuggerPort(testRandomPort(t)) 167 | auto := NewAutoGcd(s) 168 | if err := auto.Start(); err != nil { 169 | t.Fatalf("failed to start chrome: %s\n", err) 170 | } 171 | auto.SetTerminationHandler(nil) // do not want our tests to panic 172 | return auto 173 | } 174 | 175 | func testHeadlessStartup(t *testing.T) *AutoGcd { 176 | s := NewSettings(testPath, testRandomDir(t)) 177 | s.RemoveUserDir(true) 178 | s.AddStartupFlags(testStartupFlags) 179 | headlessFlags := []string{"--headless", "--hide-scrollbars"} 180 | s.AddStartupFlags(headlessFlags) 181 | s.SetDebuggerPort(testRandomPort(t)) 182 | auto := NewAutoGcd(s) 183 | if err := auto.Start(); err != nil { 184 | t.Fatalf("failed to start chrome: %s\n", err) 185 | } 186 | auto.SetTerminationHandler(nil) // do not want our tests to panic 187 | return auto 188 | } 189 | 190 | func testServer() { 191 | testListener, _ = net.Listen("tcp", ":0") 192 | _, testServerPort, _ := net.SplitHostPort(testListener.Addr().String()) 193 | testServerAddr = fmt.Sprintf("http://localhost:%s/", testServerPort) 194 | go http.Serve(testListener, http.FileServer(http.Dir("testdata"))) 195 | } 196 | 197 | func testRandomPort(t *testing.T) string { 198 | l, err := net.Listen("tcp", ":0") 199 | if err != nil { 200 | t.Fatal(err) 201 | } 202 | _, randPort, _ := net.SplitHostPort(l.Addr().String()) 203 | l.Close() 204 | return randPort 205 | } 206 | 207 | func testRandomDir(t *testing.T) string { 208 | dir, err := ioutil.TempDir(testDir, "autogcd") 209 | if err != nil { 210 | t.Fatalf("error getting temp dir: %s\n", err) 211 | } 212 | return dir 213 | } 214 | 215 | func testInstanceStartup(t *testing.T) (*AutoGcd, *exec.Cmd) { 216 | // ta := testDefaultStartup(t) 217 | port := testRandomPort(t) 218 | userDir := testRandomDir(t) 219 | flags := append(testStartupFlags, fmt.Sprintf("--remote-debugging-port=%s", port)) 220 | flags = append(flags, fmt.Sprintf("--user-data-dir=%s", userDir)) 221 | cmd := exec.Command(testPath, flags...) 222 | err := cmd.Start() 223 | if err != nil { 224 | log.Printf("start chrome ret err %+v", err) 225 | return nil, nil 226 | } 227 | s := NewSettings("", "") 228 | 229 | s.SetInstance("localhost", port) 230 | 231 | auto := NewAutoGcd(s) 232 | auto.Start() 233 | if err := auto.Start(); err != nil { 234 | t.Fatalf("failed to start chrome: %s\n", err) 235 | } 236 | auto.SetTerminationHandler(nil) // do not want our tests to panic 237 | return auto, cmd 238 | } 239 | 240 | func TestInstanceGetTab(t *testing.T) { 241 | var err error 242 | var tab *Tab 243 | auto, cmd := testInstanceStartup(t) 244 | defer func() { auto.Shutdown(); cmd.Process.Kill() }() 245 | 246 | tab, err = auto.GetTab() 247 | if err != nil { 248 | t.Fatalf("Error getting tab: %s\n", err) 249 | } 250 | 251 | if tab.Target.Type != "page" { 252 | t.Fatalf("Got tab but wasn't of type Page") 253 | } 254 | } 255 | 256 | func TestInstanceNewTab(t *testing.T) { 257 | //var newTab *Tab 258 | auto, cmd := testInstanceStartup(t) 259 | defer func() { auto.Shutdown(); cmd.Process.Kill() }() 260 | 261 | tabLen := len(auto.tabs) 262 | _, err := auto.NewTab() 263 | if err != nil { 264 | t.Fatalf("error creating new tab: %s\n", err) 265 | } 266 | 267 | if tabLen+1 != len(auto.tabs) { 268 | t.Fatalf("error created new tab but not reflected in our map") 269 | } 270 | } 271 | 272 | func TestInstanceCloseTab(t *testing.T) { 273 | var err error 274 | var newTab *Tab 275 | auto, cmd := testInstanceStartup(t) 276 | defer func() { auto.Shutdown(); cmd.Process.Kill() }() 277 | 278 | tabLen := len(auto.tabs) 279 | 280 | newTab, err = auto.NewTab() 281 | if err != nil { 282 | t.Fatalf("error creating new tab: %s\n", err) 283 | } 284 | 285 | if tabLen+1 != len(auto.tabs) { 286 | t.Fatalf("error created new tab but not reflected in our map") 287 | } 288 | 289 | err = auto.CloseTab(newTab) 290 | if err != nil { 291 | t.Fatalf("error closing tab") 292 | } 293 | 294 | if _, err := auto.tabById(newTab.Target.Id); err == nil { 295 | t.Fatalf("error closed tab still in our map") 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /conditionals.go: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2017 isaac dawson 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | */ 24 | 25 | package autogcd 26 | 27 | import ( 28 | "strings" 29 | ) 30 | 31 | // Returns true when the current url equals the equalsUrl 32 | func UrlEquals(tab *Tab, equalsUrl string) ConditionalFunc { 33 | return func(tab *Tab) bool { 34 | if url, err := tab.GetCurrentUrl(); err == nil && url == equalsUrl { 35 | return true 36 | } 37 | return false 38 | } 39 | } 40 | 41 | // Returns true when the current url contains the containsUrl 42 | func UrlContains(tab *Tab, containsUrl string) ConditionalFunc { 43 | return func(tab *Tab) bool { 44 | if url, err := tab.GetCurrentUrl(); err == nil && strings.Contains(url, containsUrl) { 45 | return true 46 | } 47 | return false 48 | } 49 | } 50 | 51 | // Returns true when the page title equals the provided equalsTitle 52 | func TitleEquals(tab *Tab, equalsTitle string) ConditionalFunc { 53 | return func(tab *Tab) bool { 54 | if pageTitle, err := tab.GetTitle(); err == nil && pageTitle == equalsTitle { 55 | return true 56 | } 57 | return false 58 | } 59 | } 60 | 61 | // Returns true if the searchTitle is contained within the page title. 62 | func TitleContains(tab *Tab, searchTitle string) ConditionalFunc { 63 | return func(tab *Tab) bool { 64 | if pageTitle, err := tab.GetTitle(); err == nil && strings.Contains(pageTitle, searchTitle) { 65 | return true 66 | } 67 | return false 68 | } 69 | } 70 | 71 | // Returns true when the element exists and is ready 72 | func ElementByIdReady(tab *Tab, elementAttributeId string) ConditionalFunc { 73 | return func(tab *Tab) bool { 74 | element, _, _ := tab.GetElementById(elementAttributeId) 75 | return (element != nil) && (element.IsReady()) 76 | } 77 | } 78 | 79 | // Returns true when the element's attribute of name equals value. 80 | func ElementAttributeEquals(tab *Tab, element *Element, name, value string) ConditionalFunc { 81 | return func(tab *Tab) bool { 82 | if element.GetAttribute(name) == value { 83 | return true 84 | } 85 | return false 86 | } 87 | } 88 | 89 | // Returns true when a selector returns a valid list of elements. 90 | func ElementsBySelectorNotEmpty(tab *Tab, elementSelector string) ConditionalFunc { 91 | return func(tab *Tab) bool { 92 | eles, err := tab.GetElementsBySelector(elementSelector) 93 | if err == nil && len(eles) > 0 { 94 | return true 95 | } 96 | return false 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /examples/googler/googler.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "github.com/wirepair/autogcd" 6 | "io/ioutil" 7 | "log" 8 | "runtime" 9 | "time" 10 | ) 11 | 12 | var ( 13 | chromePath string 14 | userDir string 15 | chromePort string 16 | debug bool 17 | ) 18 | 19 | // For an excellent list of command line switches see: http://peter.sh/experiments/chromium-command-line-switches/ 20 | var startupFlags = []string{"--disable-new-tab-first-run", "--no-first-run", "--disable-translate"} 21 | var waitForTimeout = time.Second * 5 22 | var waitForRate = time.Millisecond * 25 23 | 24 | var navigationTimeout = time.Second * 10 25 | 26 | var stableAfter = time.Millisecond * 450 27 | var stabilityTimeout = time.Second * 2 28 | 29 | func init() { 30 | switch runtime.GOOS { 31 | case "windows": 32 | flag.StringVar(&chromePath, "chrome", "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe", "path to chrome") 33 | flag.StringVar(&userDir, "dir", "C:\\temp\\", "user directory") 34 | case "darwin": 35 | flag.StringVar(&chromePath, "chrome", "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", "path to chrome") 36 | flag.StringVar(&userDir, "dir", "/tmp/", "user directory") 37 | case "linux": 38 | flag.StringVar(&chromePath, "chrome", "/usr/bin/chromium-browser", "path to chrome") 39 | flag.StringVar(&userDir, "dir", "/tmp/", "user directory") 40 | } 41 | flag.StringVar(&chromePort, "port", "9222", "Debugger port") 42 | flag.BoolVar(&debug, "debug", false, "Show debug DOM node event changes") 43 | } 44 | 45 | func main() { 46 | flag.Parse() 47 | settings := autogcd.NewSettings(chromePath, randUserDir()) 48 | settings.RemoveUserDir(true) // clean up user directory after exit 49 | settings.AddStartupFlags(startupFlags) // disable new tab junk 50 | 51 | auto := autogcd.NewAutoGcd(settings) // create our automation debugger 52 | auto.Start() // start it 53 | defer auto.Shutdown() 54 | 55 | tab, err := auto.GetTab() // get the first visual tab 56 | if err != nil { 57 | log.Fatalf("error getting visual tab to work with") 58 | } 59 | configureTab(tab) 60 | 61 | if _, _, err := tab.Navigate("https://www.google.co.jp"); err != nil { 62 | log.Fatalf("error going to google: %s\n", err) 63 | } 64 | log.Printf("navigation complete") 65 | 66 | err = tab.WaitFor(waitForRate, waitForTimeout, autogcd.ElementByIdReady(tab, "lst-ib")) 67 | if err != nil { 68 | log.Println("timed out waiting for search field") 69 | } 70 | 71 | log.Printf("getting search element") 72 | ele, _, err := tab.GetElementById("lst-ib") 73 | if err != nil { 74 | log.Fatalf("error finding search element: %s\n", err) 75 | } 76 | 77 | log.Println("sending keys") 78 | err = ele.SendKeys("github gcd\n") // use \n to hit enter 79 | if err != nil { 80 | log.Fatalf("error sending keys to element: %s\n", err) 81 | } 82 | 83 | err = tab.WaitFor(waitForRate, waitForTimeout, autogcd.TitleContains(tab, "github gcd")) 84 | if err != nil { 85 | log.Println("timed out waiting for title to change to github gcd") 86 | } 87 | 88 | log.Println("waiting for stability") 89 | err = tab.WaitStable() 90 | if err != nil { 91 | log.Printf("stability timed out: %s\n", err) 92 | } 93 | 94 | log.Println("getting search elements") 95 | eles, err := tab.GetElementsBySelector("a") 96 | for _, ele := range eles { 97 | link := ele.GetAttribute("href") 98 | if link == "https://github.com/wirepair/gcd" { 99 | log.Println("found the best link there is") 100 | loadGcd(ele, tab) 101 | break 102 | } 103 | } 104 | log.Printf("Done") 105 | } 106 | 107 | // Set various timeouts 108 | func configureTab(tab *autogcd.Tab) { 109 | tab.SetNavigationTimeout(navigationTimeout) // give up after 10 seconds for navigating, default is 30 seconds 110 | tab.SetStabilityTime(stableAfter) 111 | if debug { 112 | domHandlerFn := func(tab *autogcd.Tab, change *autogcd.NodeChangeEvent) { 113 | log.Printf("change event %s occurred\n", change.EventType) 114 | } 115 | tab.GetDOMChanges(domHandlerFn) 116 | } 117 | } 118 | 119 | func loadGcd(ele *autogcd.Element, tab *autogcd.Tab) { 120 | log.Printf("clicking google link\n") 121 | err := ele.Click() 122 | if err != nil { 123 | log.Fatalf("error clicking google link: %s\n", err) 124 | } 125 | tab.WaitFor(waitForRate, waitForTimeout, autogcd.TitleContains(tab, "wirepair/gcd")) 126 | log.Printf("here we are, bask in its glory") 127 | time.Sleep(5 * time.Second) 128 | } 129 | 130 | func randUserDir() string { 131 | dir, err := ioutil.TempDir(userDir, "autogcd") 132 | if err != nil { 133 | log.Fatalf("error getting temp dir: %s\n", err) 134 | } 135 | return dir 136 | } 137 | -------------------------------------------------------------------------------- /settings.go: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2017 isaac dawson 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | */ 24 | 25 | package autogcd 26 | 27 | import ( 28 | "fmt" 29 | "time" 30 | ) 31 | 32 | type Settings struct { 33 | connectToInstance bool 34 | timeout time.Duration // timeout for giving up on chrome starting and connecting to the debugger service 35 | chromePath string // path to chrome 36 | chromeHost string // can really only be localhost 37 | chromePort string // port to chrome debugger 38 | userDir string // the user directory to use 39 | removeUserDir bool // should we delete the user directory on shutdown? 40 | extensions []string // custom extensions to load 41 | flags []string // custom os.Environ flags to use to start the chrome process 42 | env []string // custom env vars for launching the process 43 | } 44 | 45 | // Creates a new settings object to start Chrome and enable remote debugging 46 | func NewSettings(chromePath, userDir string) *Settings { 47 | s := &Settings{} 48 | s.chromePath = chromePath 49 | s.chromePort = "9222" 50 | s.userDir = userDir 51 | s.removeUserDir = false 52 | s.extensions = make([]string, 0) 53 | s.flags = make([]string, 0) 54 | s.env = make([]string, 0) 55 | return s 56 | } 57 | 58 | //Set a instance to connect to other than start a new process 59 | func (s *Settings) SetInstance(host, port string) { 60 | s.chromeHost = host 61 | s.chromePort = port 62 | s.connectToInstance = true 63 | } 64 | 65 | // Can really only be localhost, but this may change in the future so support it anyways. 66 | func (s *Settings) SetChromeHost(host string) { 67 | s.chromeHost = host 68 | } 69 | 70 | func (s *Settings) AddEnvironmentVars(env []string) { 71 | s.env = append(s.env, env...) 72 | } 73 | 74 | // Sets the chrome debugger port. 75 | func (s *Settings) SetDebuggerPort(chromePort string) { 76 | s.chromePort = chromePort 77 | } 78 | 79 | // How long to wait for chrome to startup and allow us to connect. 80 | func (s *Settings) SetStartTimeout(timeout time.Duration) { 81 | s.timeout = timeout 82 | } 83 | 84 | // On Shutdown, deletes the userDir and files if true. 85 | func (s *Settings) RemoveUserDir(shouldRemove bool) { 86 | s.removeUserDir = true 87 | } 88 | 89 | // Adds custom flags when starting the chrome process 90 | func (s *Settings) AddStartupFlags(flags []string) { 91 | s.flags = append(s.flags, flags...) 92 | } 93 | 94 | // Adds a custom extension to launch with chrome. Note this extension MAY NOT USE 95 | // the chrome.debugger API since you can not attach debuggers to a Tab twice. 96 | func (s *Settings) AddExtension(paths []string) { 97 | for _, ext := range paths { 98 | s.extensions = append(s.extensions, fmt.Sprintf("--load-extension=%s", ext)) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /tab_subscribers.go: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2017 isaac dawson 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | */ 24 | 25 | package autogcd 26 | 27 | import ( 28 | "encoding/json" 29 | "github.com/wirepair/gcd" 30 | "github.com/wirepair/gcd/gcdapi" 31 | ) 32 | 33 | func (t *Tab) subscribeTargetCrashed() { 34 | t.Subscribe("Inspector.targetCrashed", func(target *gcd.ChromeTarget, payload []byte) { 35 | select { 36 | case t.crashedCh <- "crashed": 37 | case <-t.exitCh: 38 | } 39 | }) 40 | } 41 | 42 | func (t *Tab) subscribeTargetDetached() { 43 | t.Subscribe("Inspector.detached", func(target *gcd.ChromeTarget, payload []byte) { 44 | header := &gcdapi.InspectorDetachedEvent{} 45 | err := json.Unmarshal(payload, header) 46 | reason := "detached" 47 | 48 | if err == nil { 49 | reason = header.Params.Reason 50 | } 51 | 52 | select { 53 | case t.crashedCh <- reason: 54 | case <-t.exitCh: 55 | } 56 | }) 57 | } 58 | 59 | // our default loadFiredEvent handler, returns a response to resp channel to navigate once complete. 60 | func (t *Tab) subscribeLoadEvent() { 61 | t.Subscribe("Page.loadEventFired", func(target *gcd.ChromeTarget, payload []byte) { 62 | if t.IsNavigating() { 63 | select { 64 | case t.navigationCh <- 0: 65 | case <-t.exitCh: 66 | } 67 | 68 | } 69 | }) 70 | } 71 | 72 | func (t *Tab) subscribeFrameLoadingEvent() { 73 | t.Subscribe("Page.frameStartedLoading", func(target *gcd.ChromeTarget, payload []byte) { 74 | t.debugf("frameStartedLoading: %s\n", string(payload)) 75 | if t.IsNavigating() { 76 | return 77 | } 78 | header := &gcdapi.PageFrameStartedLoadingEvent{} 79 | err := json.Unmarshal(payload, header) 80 | // has the top frame id begun navigating? 81 | if err == nil && header.Params.FrameId == t.GetTopFrameId() { 82 | t.setIsTransitioning(true) 83 | } 84 | }) 85 | } 86 | 87 | func (t *Tab) subscribeFrameFinishedEvent() { 88 | t.Subscribe("Page.frameStoppedLoading", func(target *gcd.ChromeTarget, payload []byte) { 89 | t.debugf("frameStoppedLoading: %s\n", string(payload)) 90 | if t.IsNavigating() { 91 | return 92 | } 93 | header := &gcdapi.PageFrameStoppedLoadingEvent{} 94 | err := json.Unmarshal(payload, header) 95 | // has the top frame id begun navigating? 96 | if err == nil && header.Params.FrameId == t.GetTopFrameId() { 97 | t.setIsTransitioning(false) 98 | } 99 | }) 100 | } 101 | 102 | func (t *Tab) subscribeSetChildNodes() { 103 | // new nodes 104 | t.Subscribe("DOM.setChildNodes", func(target *gcd.ChromeTarget, payload []byte) { 105 | header := &gcdapi.DOMSetChildNodesEvent{} 106 | err := json.Unmarshal(payload, header) 107 | if err == nil { 108 | event := header.Params 109 | t.dispatchNodeChange(&NodeChangeEvent{EventType: SetChildNodesEvent, Nodes: event.Nodes, ParentNodeId: event.ParentId}) 110 | 111 | } 112 | }) 113 | } 114 | 115 | func (t *Tab) subscribeAttributeModified() { 116 | t.Subscribe("DOM.attributeModified", func(target *gcd.ChromeTarget, payload []byte) { 117 | header := &gcdapi.DOMAttributeModifiedEvent{} 118 | err := json.Unmarshal(payload, header) 119 | if err == nil { 120 | event := header.Params 121 | t.dispatchNodeChange(&NodeChangeEvent{EventType: AttributeModifiedEvent, Name: event.Name, Value: event.Value, NodeId: event.NodeId}) 122 | } 123 | }) 124 | } 125 | 126 | func (t *Tab) subscribeAttributeRemoved() { 127 | t.Subscribe("DOM.attributeRemoved", func(target *gcd.ChromeTarget, payload []byte) { 128 | header := &gcdapi.DOMAttributeRemovedEvent{} 129 | err := json.Unmarshal(payload, header) 130 | if err == nil { 131 | event := header.Params 132 | t.dispatchNodeChange(&NodeChangeEvent{EventType: AttributeRemovedEvent, NodeId: event.NodeId, Name: event.Name}) 133 | } 134 | }) 135 | } 136 | func (t *Tab) subscribeCharacterDataModified() { 137 | t.Subscribe("DOM.characterDataModified", func(target *gcd.ChromeTarget, payload []byte) { 138 | header := &gcdapi.DOMCharacterDataModifiedEvent{} 139 | err := json.Unmarshal(payload, header) 140 | if err == nil { 141 | event := header.Params 142 | t.dispatchNodeChange(&NodeChangeEvent{EventType: CharacterDataModifiedEvent, NodeId: event.NodeId, CharacterData: event.CharacterData}) 143 | } 144 | }) 145 | } 146 | func (t *Tab) subscribeChildNodeCountUpdated() { 147 | t.Subscribe("DOM.childNodeCountUpdated", func(target *gcd.ChromeTarget, payload []byte) { 148 | header := &gcdapi.DOMChildNodeCountUpdatedEvent{} 149 | err := json.Unmarshal(payload, header) 150 | if err == nil { 151 | event := header.Params 152 | t.dispatchNodeChange(&NodeChangeEvent{EventType: ChildNodeCountUpdatedEvent, NodeId: event.NodeId, ChildNodeCount: event.ChildNodeCount}) 153 | } 154 | }) 155 | } 156 | func (t *Tab) subscribeChildNodeInserted() { 157 | t.Subscribe("DOM.childNodeInserted", func(target *gcd.ChromeTarget, payload []byte) { 158 | //log.Printf("childNodeInserted: %s\n", string(payload)) 159 | header := &gcdapi.DOMChildNodeInsertedEvent{} 160 | err := json.Unmarshal(payload, header) 161 | if err == nil { 162 | event := header.Params 163 | t.dispatchNodeChange(&NodeChangeEvent{EventType: ChildNodeInsertedEvent, Node: event.Node, ParentNodeId: event.ParentNodeId, PreviousNodeId: event.PreviousNodeId}) 164 | } 165 | }) 166 | } 167 | func (t *Tab) subscribeChildNodeRemoved() { 168 | t.Subscribe("DOM.childNodeRemoved", func(target *gcd.ChromeTarget, payload []byte) { 169 | header := &gcdapi.DOMChildNodeRemovedEvent{} 170 | err := json.Unmarshal(payload, header) 171 | if err == nil { 172 | event := header.Params 173 | t.dispatchNodeChange(&NodeChangeEvent{EventType: ChildNodeRemovedEvent, ParentNodeId: event.ParentNodeId, NodeId: event.NodeId}) 174 | } 175 | }) 176 | } 177 | 178 | func (t *Tab) dispatchNodeChange(evt *NodeChangeEvent) { 179 | select { 180 | case t.nodeChange <- evt: 181 | case <-t.exitCh: 182 | return 183 | } 184 | } 185 | 186 | /* 187 | func (t *Tab) subscribeInlineStyleInvalidated() { 188 | t.Subscribe("DOM.inlineStyleInvalidatedEvent", func(target *gcd.ChromeTarget, payload []byte) { 189 | event := &gcdapi.DOMInlineStyleInvalidatedEvent{} 190 | err := json.Unmarshal(payload, header) 191 | if err == nil { 192 | event = header.Params 193 | t.nodeChange <- &NodeChangeEvent{EventType: InlineStyleInvalidatedEvent, NodeIds: event.NodeIds} 194 | } 195 | }) 196 | } 197 | */ 198 | func (t *Tab) subscribeDocumentUpdated() { 199 | // node ids are no longer valid 200 | t.Subscribe("DOM.documentUpdated", func(target *gcd.ChromeTarget, payload []byte) { 201 | select { 202 | case t.nodeChange <- &NodeChangeEvent{EventType: DocumentUpdatedEvent}: 203 | case <-t.exitCh: 204 | } 205 | }) 206 | } 207 | -------------------------------------------------------------------------------- /testdata/attributes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | button click 5 | 9 | 10 | 11 |
12 | 13 |
14 | 15 | -------------------------------------------------------------------------------- /testdata/background.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | background 6 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /testdata/big_body.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | autogcd big body test 6 | 10 | 11 | 12 |
hi
13 |


































































































































































































14 |
buh bye
15 | 16 | 17 | -------------------------------------------------------------------------------- /testdata/button.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | button click 5 | 13 | 14 | 15 |
button click
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /testdata/console_log.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | console log 5 | 10 | 11 | 12 |
console log
13 | 14 | -------------------------------------------------------------------------------- /testdata/cookie1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | script inject test 5 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /testdata/cookie2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | script inject test 5 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /testdata/dblclick.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | button click 5 | 13 | 14 | 15 |
double click
16 | 17 | -------------------------------------------------------------------------------- /testdata/events.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | event listeners 5 | 23 | 24 | 25 |
button click
26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /testdata/frame_a.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | frame a 4 | 5 | 6 |
frame a
7 | 8 | -------------------------------------------------------------------------------- /testdata/frame_b.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | frame b 4 | 5 | 6 |
frame b
7 | 8 | -------------------------------------------------------------------------------- /testdata/frame_top.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | frame has redirect occur 4 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /testdata/frameset.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | frameset top 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /testdata/iframe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | IFR SRC 4 | 11 | 14 | 15 | 16 | BLAH BLAH BODY BLAH BLAH 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /testdata/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | autogcd test 6 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /testdata/inner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | inner frame document modification 4 | 12 | 13 | 14 | 15 |
blerp
16 | 17 | 18 | -------------------------------------------------------------------------------- /testdata/input.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | input test 5 | 15 | 16 | 17 |
18 | 19 |
20 | 21 | -------------------------------------------------------------------------------- /testdata/invalidated.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | document modification 4 | 14 | 15 | 16 |
17 |
blerp
18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /testdata/mouseover.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | mouse over 5 | 13 | 14 | 15 |
button click
16 | 17 | 18 | -------------------------------------------------------------------------------- /testdata/prompt.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | prompt test 5 | 11 | 12 | 13 |
14 | 15 | -------------------------------------------------------------------------------- /testdata/redirect.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | document modification 4 | 17 | 18 | 19 |
20 |
see ya!
21 |
22 | 23 | 24 | -------------------------------------------------------------------------------- /testdata/redirect_target.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | target of redirect 4 | 14 | 15 | 16 |
17 |
i'm a target page
18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /testdata/script.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | script inject test 5 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /testdata/script_inner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | script inject inner test 5 | 9 | 10 | 11 |
inner iframe
12 | 13 | -------------------------------------------------------------------------------- /testdata/scroll.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | button click 5 | 9 | 10 | 11 |
yehp
12 | 13 | -------------------------------------------------------------------------------- /testdata/table.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | table test 6 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
1112
2122
3132
4142
32 | 33 | 34 | -------------------------------------------------------------------------------- /testdata/window_main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | window main 6 | 12 | 13 | 14 |
main window
15 | 16 | 17 | -------------------------------------------------------------------------------- /testdata/window_sub1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | window main 6 | 12 | 13 | 14 |
sub 1 window
15 | 16 | 17 | -------------------------------------------------------------------------------- /testdata/window_sub2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | window main 6 | 10 | 11 | 12 |
sub 2 window
13 | 14 | 15 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2017 isaac dawson 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | */ 24 | 25 | package autogcd 26 | 27 | import ( 28 | "github.com/wirepair/gcd/gcdapi" 29 | ) 30 | 31 | // Common node types 32 | type NodeType uint8 33 | 34 | const ( 35 | ELEMENT_NODE NodeType = 0x1 36 | TEXT_NODE NodeType = 0x3 37 | PROCESSING_INSTRUCTION_NODE NodeType = 0x7 38 | COMMENT_NODE NodeType = 0x8 39 | DOCUMENT_NODE NodeType = 0x9 40 | DOCUMENT_TYPE_NODE NodeType = 0x10 41 | DOCUMENT_FRAGMENT_NODE NodeType = 0x11 42 | ) 43 | 44 | var nodeTypeMap = map[NodeType]string{ 45 | ELEMENT_NODE: "ELEMENT_NODE", 46 | TEXT_NODE: "TEXT_NODE", 47 | PROCESSING_INSTRUCTION_NODE: "PROCESSING_INSTRUCTION_NODE", 48 | COMMENT_NODE: "COMMENT_NODE", 49 | DOCUMENT_NODE: "DOCUMENT_NODE", 50 | DOCUMENT_TYPE_NODE: "DOCUMENT_TYPE_NODE", 51 | DOCUMENT_FRAGMENT_NODE: "DOCUMENT_FRAGMENT_NODE", 52 | } 53 | 54 | // Document/Node change event types 55 | type ChangeEventType uint16 56 | 57 | const ( 58 | DocumentUpdatedEvent ChangeEventType = 0x0 59 | SetChildNodesEvent ChangeEventType = 0x1 60 | AttributeModifiedEvent ChangeEventType = 0x2 61 | AttributeRemovedEvent ChangeEventType = 0x3 62 | InlineStyleInvalidatedEvent ChangeEventType = 0x4 63 | CharacterDataModifiedEvent ChangeEventType = 0x5 64 | ChildNodeCountUpdatedEvent ChangeEventType = 0x6 65 | ChildNodeInsertedEvent ChangeEventType = 0x7 66 | ChildNodeRemovedEvent ChangeEventType = 0x8 67 | ) 68 | 69 | var changeEventMap = map[ChangeEventType]string{ 70 | DocumentUpdatedEvent: "DocumentUpdatedEvent", 71 | SetChildNodesEvent: "SetChildNodesEvent", 72 | AttributeModifiedEvent: "AttributeModifiedEvent", 73 | AttributeRemovedEvent: "AttributeRemovedEvent", 74 | InlineStyleInvalidatedEvent: "InlineStyleInvalidatedEvent", 75 | CharacterDataModifiedEvent: "CharacterDataModifiedEvent", 76 | ChildNodeCountUpdatedEvent: "ChildNodeCountUpdatedEvent", 77 | ChildNodeInsertedEvent: "ChildNodeInsertedEvent", 78 | ChildNodeRemovedEvent: "ChildNodeRemovedEvent", 79 | } 80 | 81 | func (evt ChangeEventType) String() string { 82 | if s, ok := changeEventMap[evt]; ok { 83 | return s 84 | } 85 | return "" 86 | } 87 | 88 | // For handling DOM updating nodes 89 | type NodeChangeEvent struct { 90 | EventType ChangeEventType // the type of node change event 91 | NodeId int // nodeid of change 92 | NodeIds []int // nodeid of changes for inlinestyleinvalidated 93 | ChildNodeCount int // updated childnodecount event 94 | Nodes []*gcdapi.DOMNode // Child nodes array. for setChildNodesEvent 95 | Node *gcdapi.DOMNode // node for child node inserted event 96 | Name string // attribute name 97 | Value string // attribute value 98 | CharacterData string // new text value for characterDataModified events 99 | ParentNodeId int // node id for setChildNodesEvent, childNodeInsertedEvent and childNodeRemovedEvent 100 | PreviousNodeId int // previous node id for childNodeInsertedEvent 101 | 102 | } 103 | 104 | // Outbound network requests 105 | type NetworkRequest struct { 106 | RequestId string // Internal chrome request id 107 | FrameId string // frame that the request went out on 108 | LoaderId string // internal chrome loader id 109 | DocumentURL string // url of the frame 110 | Request *gcdapi.NetworkRequest // underlying Request object 111 | Timestamp float64 // time the request was dispatched 112 | Initiator *gcdapi.NetworkInitiator // who initiated the request 113 | RedirectResponse *gcdapi.NetworkResponse // non-nil if it was a redirect 114 | Type string // Document, Stylesheet, Image, Media, Font, Script, TextTrack, XHR, Fetch, EventSource, WebSocket, Other 115 | } 116 | 117 | // Inbound network responses 118 | type NetworkResponse struct { 119 | RequestId string // Internal chrome request id 120 | FrameId string // frame that the request went out on 121 | LoaderId string // internal chrome loader id 122 | Response *gcdapi.NetworkResponse // underlying Response object 123 | Timestamp float64 // time the request was received 124 | Type string // Document, Stylesheet, Image, Media, Font, Script, TextTrack, XHR, Fetch, EventSource, WebSocket, Other 125 | } 126 | 127 | // For storage related events. 128 | type StorageEventType uint16 129 | 130 | type StorageEvent struct { 131 | IsLocalStorage bool // if true, local storage, false session storage 132 | SecurityOrigin string // origin that this event occurred on 133 | Key string // storage key 134 | NewValue string // new storage value 135 | OldValue string // old storage value 136 | } 137 | -------------------------------------------------------------------------------- /vendor/github.com/wirepair/gcd/.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /vendor/github.com/wirepair/gcd/.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # ========================= 18 | # Operating System Files 19 | # ========================= 20 | 21 | # OSX 22 | # ========================= 23 | 24 | .DS_Store 25 | .AppleDouble 26 | .LSOverride 27 | 28 | # Icon must ends with two \r. 29 | Icon 30 | 31 | 32 | # Thumbnails 33 | ._* 34 | 35 | # Files that might appear on external disk 36 | .Spotlight-V100 37 | .Trashes 38 | 39 | *.exe 40 | debug.log 41 | 42 | output 43 | 44 | gcdapigen/gcdapigen 45 | -------------------------------------------------------------------------------- /vendor/github.com/wirepair/gcd/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog (2018) 2 | - December 8th: Updated to latest gcd / protocol.json file for 71.0.3578.80 3 | - October 18th: Updated to latest gcd / protocol.json file for 70.0.3538.67. 4 | - October 17th: Added new helper features to Gcd, GetFirstTab()) and DeleteProfileOnExit() 5 | - October 4th: Updated to latest gcd / protocol.json file for 69.0.3497.100 for stable branch. Fixed bug in gcdapigen when resolving references to references to base types. 6 | - June 1st: Updated to latest gcd / protocol.json file for 67.0.3396.62 for *stable* branch. 7 | - April 24th: Updated to latest gcd / protocol.json file for 66.0.3359.117 for *stable* branch. Please note there was a change to protocol files again, more details: https://github.com/wirepair/gcd/issues/21. 8 | - Feburary 22nd: Updated to latest gcd / protocol.json file for 66.0.3346.8. Added dep init/dep ensure to repository for package management. 9 | - January 17th: Updated to latest protocol. Removed examples from README.md because API changes too often, added a link to examples instead which I ensure work after update. Fixed \n in descriptions of protocol.json files causing template generation errors. Current is 65.0.3322.3. Updated examples to work with latest API changes. 10 | 11 | # Changelog (2017) 12 | - November 20th: Updated to latest protocol. Current is 64.0.3269.3. Updated tests and examples to work with latest API changes. 13 | - October 30th: Updated to latest protocol. Current is 64.0.3251.0. Updated examples to work with latest API changes. 14 | - September 9th: Updated to latest protocol files. 15 | We now download specific channels protocol files. By default we download the 'dev' channel. You can override this if you wish in gcdapigen by passing gcdapigen -update -channel {canary,stable,...}. To see what version gcd is bound to, check the gcdapi/version.go file, or call gcd.GetRevision(). Current is 62.0.3202.9. 16 | - August 15th: Updated to the latest protocol.json 17 | - July 8th: Updated to latest protocol.json which supports network interception. However it does not appear to work in most versions yet. (I could only get it working in the latest https://download-chromium.appspot.com/ download.) Also, going forward GCDVERSION will be Major.Year.Month.Day.Minor format. 18 | - June: Due to the excellent efforts of [Ian L.](https://github.com/MrSaints) gcd can now issue requests ignoring fields. A non-breaking change was implemented allowing callers to use new methods (denoted by WithParams) to pass in structures, and choosing which fields to populate. Users can continue to use the old method passing individual arguments. 19 | Example: 20 | ```Go 21 | networkParams := &gcdapi.NetworkEnableParams{ 22 | MaxTotalBufferSize: -1, 23 | MaxResourceBufferSize: -1, 24 | } 25 | 26 | if _, err := network.EnableWithParams(networkParams); err != nil { 27 | log.Fatal("error enabling network") 28 | } 29 | ``` 30 | - May: Updated to latest protocol.json 31 | - May: Fixed a bug with templating causing certain parameters that were slices to only show up as the base type. 32 | - May: Changed templating where 'any' protocol.json field types from string to interface{} to allow caller to decide how to decode. 33 | - April: Updated to the latest protocol.json (version 1.2). Note this changes quite a few APIs. 34 | 35 | # Changelog (2016) 36 | - June: Updated to the latest protocol.json, gcdapigen will download the js_protocol and browser_protocol json files from chromium repositories. It will also fix them up and merge them into a single file and output it. 37 | Note that several API endpoints have been removed and method calls have changed since the last update. 38 | - February: I created a new library for actual automation purposes. If you want something with more functionality and more usability I suggest checking out [autogcd](https://github.com/wirepair/autogcd). -------------------------------------------------------------------------------- /vendor/github.com/wirepair/gcd/Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | branch = "master" 6 | name = "golang.org/x/net" 7 | packages = ["websocket"] 8 | revision = "610586996380ceef02dd726cc09df7e00a3f8e56" 9 | 10 | [solve-meta] 11 | analyzer-name = "dep" 12 | analyzer-version = 1 13 | inputs-digest = "e3a5fa808a45f7d729055df7d9ccd68c0a05412e805bcd8a6d15d46598ab2993" 14 | solver-name = "gps-cdcl" 15 | solver-version = 1 16 | -------------------------------------------------------------------------------- /vendor/github.com/wirepair/gcd/Gopkg.toml: -------------------------------------------------------------------------------- 1 | # Gopkg.toml example 2 | # 3 | # Refer to https://golang.github.io/dep/docs/Gopkg.toml.html 4 | # for detailed Gopkg.toml documentation. 5 | # 6 | # required = ["github.com/user/thing/cmd/thing"] 7 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 8 | # 9 | # [[constraint]] 10 | # name = "github.com/user/project" 11 | # version = "1.0.0" 12 | # 13 | # [[constraint]] 14 | # name = "github.com/user/project2" 15 | # branch = "dev" 16 | # source = "github.com/myfork/project2" 17 | # 18 | # [[override]] 19 | # name = "github.com/x/y" 20 | # version = "2.4.0" 21 | # 22 | # [prune] 23 | # non-go = false 24 | # go-tests = true 25 | # unused-packages = true 26 | 27 | 28 | [[constraint]] 29 | branch = "master" 30 | name = "golang.org/x/net" 31 | 32 | [prune] 33 | go-tests = true 34 | unused-packages = true 35 | -------------------------------------------------------------------------------- /vendor/github.com/wirepair/gcd/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 isaac dawson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /vendor/github.com/wirepair/gcd/README.md: -------------------------------------------------------------------------------- 1 | [![Go Report Card](https://goreportcard.com/badge/github.com/wirepair/gcd)](https://goreportcard.com/report/github.com/wirepair/gcd) 2 | 3 | # Google Chrome Debugger (GCD) 4 | This is primarly an auto-generated client library for communicating with a Google Chrome Browser over their [remote client debugger protocol](https://developer.chrome.com/devtools/docs/debugger-protocol). Note that their documentation is partially incorrect and does not contain a lot of the API calls that are actually available. 5 | 6 | Because I'm lazy and there are hundereds of different custom types and API methods, this library has been automatically generated using their [protocol.json](https://code.google.com/p/chromium/codesearch#chromium/src/third_party/WebKit/Source/devtools/protocol.json&q=protocol.json&sq=package:chromium&type=cs). 7 | 8 | The [gcdapigen](https://github.com/wirepair/gcd/tree/master/gcdapigen) program was created to generate types, event types and commands for gcd. 9 | 10 | # Changelog 11 | See the [CHANGELOG](https://github.com/wirepair/gcd/tree/master/CHANGELOG.md). 12 | 13 | ## Dependencies 14 | gcd requires the [gcdapi](https://github.com/wirepair/gcd/tree/master/gcdapi) and [gcdmessage](https://github.com/wirepair/gcd/tree/master/gcdmessage) packages. gcdapi is the auto-generated API. gcdmessage is the glue between gcd and gcdapi so we can keep the packages clean. 15 | 16 | ## The API 17 | The API consists of of synchronous requests, asynchronous requests / events. Synchronous requests are handled by using non-buffered channels and methods can be called and will return once the value is available. Events are handled by subscribing the response method type and calling the API's "Enable()" such as: 18 | ```Go 19 | target, err := debugger.NewTab() 20 | if err != nil { 21 | log.Fatalf("error getting new tab: %s\n", err) 22 | } 23 | console := target.Console 24 | 25 | target.Subscribe("Console.messageAdded", func(target *ChromeTarget, v []byte) { 26 | 27 | msg := &gcdapi.ConsoleMessageAddedEvent{} 28 | err := json.Unmarshal(v, msg) 29 | if err != nil { 30 | log.Fatalf("error unmarshalling event data: %v\n", err) 31 | } 32 | log.Printf("METHOD: %s\n", msg.Method) 33 | eventData := msg.Params.Message 34 | log.Printf("Got event: %s\n", eventData) 35 | }) 36 | console.Enable() 37 | // recv events 38 | // console.Disable() 39 | ``` 40 | 41 | ## Usage 42 | For a full list of api methods, types, event types & godocs: [Documentation](https://godoc.org/github.com/wirepair/gcd/gcdapi) 43 | 44 | ## Examples 45 | See [examples](https://github.com/wirepair/gcd/tree/master/examples) 46 | 47 | ## Licensing 48 | The MIT License (MIT) 49 | 50 | Copyright (c) 2018 isaac dawson 51 | 52 | Permission is hereby granted, free of charge, to any person obtaining a copy 53 | of this software and associated documentation files (the "Software"), to deal 54 | in the Software without restriction, including without limitation the rights 55 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 56 | copies of the Software, and to permit persons to whom the Software is 57 | furnished to do so, subject to the following conditions: 58 | 59 | The above copyright notice and this permission notice shall be included in 60 | all copies or substantial portions of the Software. 61 | 62 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 63 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 64 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 65 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 66 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 67 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 68 | THE SOFTWARE. 69 | -------------------------------------------------------------------------------- /vendor/github.com/wirepair/gcd/gcdapi/README.md: -------------------------------------------------------------------------------- 1 | # gcdapi 2 | This package contains the automatically generated API types, events and commands for the Google Chrome Debugger service. Please see [gcdapigen](https://github.com/wirepair/gcd/tree/master/gcdapigen) for more details. 3 | 4 | ## Licensing 5 | The MIT License (MIT) 6 | 7 | Copyright (c) 2015 isaac dawson 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in 17 | all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | THE SOFTWARE. -------------------------------------------------------------------------------- /vendor/github.com/wirepair/gcd/gcdapi/accessibility.go: -------------------------------------------------------------------------------- 1 | // AUTO-GENERATED Chrome Remote Debugger Protocol API Client 2 | // This file contains Accessibility functionality. 3 | // API Version: 1.3 4 | 5 | package gcdapi 6 | 7 | import ( 8 | "encoding/json" 9 | "github.com/wirepair/gcd/gcdmessage" 10 | ) 11 | 12 | // A single source for a computed AX property. 13 | type AccessibilityAXValueSource struct { 14 | Type string `json:"type"` // What type of source this is. enum values: attribute, implicit, style, contents, placeholder, relatedElement 15 | Value *AccessibilityAXValue `json:"value,omitempty"` // The value of this property source. 16 | Attribute string `json:"attribute,omitempty"` // The name of the relevant attribute, if any. 17 | AttributeValue *AccessibilityAXValue `json:"attributeValue,omitempty"` // The value of the relevant attribute, if any. 18 | Superseded bool `json:"superseded,omitempty"` // Whether this source is superseded by a higher priority source. 19 | NativeSource string `json:"nativeSource,omitempty"` // The native markup source for this value, e.g. a